Is it possible to have hibernate generate update from values statements for postgresql?

646 views Asked by At

Given a postgresql table

               Table "public.test"  
  Column  |            Type             | Modifiers   
----------+-----------------------------+-----------  
 id       | integer                     | not null  
 info     | text                        |   

And the following values :

# select * from test;  
 id |     info     
----+--------------
  3 | value3        
  4 | value4
  5 | value5

As you may know with postgresql you can use this kind of statements to update multiples rows with different values :

update test set info=tmp.info from (values (3,'newvalue3'),(4,'newvalue4'),(5,'newvalue5')) as tmp (id,info) where test.id=tmp.id; 

And it results in the table being updated in a single queries to :

# select * from test;  
 id |     info     
----+--------------
  3 | newvalue3        
  4 | newvalue4
  5 | newvalue5

I have been looking around everywhere as to how to make hibernate generate this kind of statements for update queries. I know how to make it work for insert queries (with reWriteBatchedInserts jdbc option and hibernate batch config options).

But is it possible for update queries or do I have to write the native query myself ? No matter what I do, hibernate always sends separate update queries to the database (I'm looking to the postgresql server statements logs for this affirmation).

2020-06-18 08:19:48.895 UTC [1642] LOG:  execute S_6: BEGIN
2020-06-18 08:19:48.895 UTC [1642] LOG:  execute S_8: update test set info = $1 where id = $2
2020-06-18 08:19:48.895 UTC [1642] DETAIL:  parameters: $1 = 'newvalue3', $2 = '3'
2020-06-18 08:19:48.896 UTC [1642] LOG:  execute S_8: update test set info = $1 where id = $2
2020-06-18 08:19:48.896 UTC [1642] DETAIL:  parameters: $1 = 'newvalue4', $2 = '4'
2020-06-18 08:19:48.896 UTC [1642] LOG:  execute S_8: update test set info = $1 where id = $2
2020-06-18 08:19:48.896 UTC [1642] DETAIL:  parameters: $1 = 'newvalue4', $2 = '5'
2020-06-18 08:19:48.896 UTC [1642] LOG:  execute S_1: COMMIT

I always find it many times faster to issue a single massive update query than many separate update targeting single rows. With many seperate update queries, even though they are sent in a batch by the jdbc driver, they still need to be processed sequentially by the server, so it is not as efficient as a single update query targeting multiples rows. So if anyone has a solution that wouldn't involve writing native queries for my entities, I would be very glad !


Update

To further refine my question I want to add a clarification. I'm looking for a solution that wouldn't abandon Hibernate dirty checking feature for entities updates. I'm trying to avoid to write batch update queries by hand for the general case of having to updating a few basic fields with different values on an entity list. I'm currently looking into the SPI of hibernate to see it if it's doable. org.hibernate.engine.jdbc.batch.spi.Batch seems to be the proper place but I'm not quite sure yet because I've never done anything with hibernate SPI). Any insights would be welcomed !


1

There are 1 answers

6
Christian Beikov On

You can use Blaze-Persistence for this which is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model.

It does not yet support the FROM clause in DML, but that is about to land in the next release: https://github.com/Blazebit/blaze-persistence/issues/693

Meanwhile you could use CTEs for this. First you need to define a CTE entity(a concept of Blaze-Persistence):

@CTE
@Entity
public class InfoCte {
  @Id Integer id;
  String info;
}

I'm assuming your entity model looks roughly like this

@Entity
public class Test {
  @Id Integer id;
  String info;
}

Then you can use Blaze-Persistence like this:

criteriaBuilderFactory.update(entityManager, Test.class, "test")
  .with(InfoCte.class, false)
    .fromValues(Test.class, "newInfos", newInfosCollection)
    .bind("id").select("newInfos.id")
    .bind("info").select("newInfos.info")
  .end()
  .set("info")
    .from(InfoCte.class, "cte")
    .select("cte.info")
    .where("cte.id").eqExpression("test.id")
  .end()
  .whereExists()
    .from(InfoCte.class, "cte")
    .where("cte.id").eqExpression("test.id")
  .end()
.executeUpdate();

This will create an SQL query similar to the following

WITH InfoCte(id, info) AS(
  SELECT t.id, t.info
  FROM (VALUES(1, 'newValue', ...)) t(id, info)
)
UPDATE test
SET info = (SELECT cte.info FROM InfoCte cte WHERE cte.id = test.id)
WHERE EXISTS (SELECT 1 FROM InfoCte cte WHERE cte.id = test.id)