12. Batching

12.1. JDBC batching

JDBC offers support for batching together SQL statements that can be represented as a single PreparedStatement. Implementation wise this generally means that drivers will send the batched operation to the server in one call, which can save on network calls to the database. Hibernate can leverage JDBC batching. The following settings control this behavior.

hibernate.jdbc.batch_size

Controls the maximum number of statements Hibernate will batch together before asking the driver to execute the batch. Zero or a negative number disables this feature.

hibernate.jdbc.batch_versioned_data

Some JDBC drivers return incorrect row counts when a batch is executed. If your JDBC driver falls into this category this setting should be set to false. Otherwise, it is safe to enable this which will allow Hibernate to still batch the DML for versioned entities and still use the returned row counts for optimistic lock checks. Since 5.0, it defaults to true. Previously (versions 3.x and 4.x), it used to be false.

hibernate.jdbc.batch.builder

Names the implementation class used to manage batching capabilities. It is almost never a good idea to switch from Hibernate’s default implementation. But if you wish to, this setting would name the org.hibernate.engine.jdbc.batch.spi.BatchBuilder implementation to use.

hibernate.order_updates

Forces Hibernate to order SQL updates by the entity type and the primary key value of the items being updated. This allows for more batching to be used. It will also result in fewer transaction deadlocks in highly concurrent systems. Comes with a performance hit, so benchmark before and after to see if this actually helps or hurts your application.

hibernate.order_inserts

Forces Hibernate to order inserts to allow for more batching to be used. Comes with a performance hit, so benchmark before and after to see if this actually helps or hurts your application.

Since version 5.2, Hibernate allows overriding the global JDBC batch size given by the hibernate.jdbc.batch_size configuration property on a per Session basis.

Example 430. Hibernate specific JDBC batch size configuration on a per Session basis

  1. entityManager
  2. .unwrap( Session.class )
  3. .setJdbcBatchSize( 10 );

12.2. Session batching

The following example shows an anti-pattern for batch inserts.

Example 431. Naive way to insert 100 000 entities with Hibernate

  1. EntityManager entityManager = null;
  2. EntityTransaction txn = null;
  3. try {
  4. entityManager = entityManagerFactory().createEntityManager();
  5. txn = entityManager.getTransaction();
  6. txn.begin();
  7. for ( int i = 0; i < 100_000; i++ ) {
  8. Person Person = new Person( String.format( "Person %d", i ) );
  9. entityManager.persist( Person );
  10. }
  11. txn.commit();
  12. } catch (RuntimeException e) {
  13. if ( txn != null && txn.isActive()) txn.rollback();
  14. throw e;
  15. } finally {
  16. if (entityManager != null) {
  17. entityManager.close();
  18. }
  19. }

There are several problems associated with this example:

  1. Hibernate caches all the newly inserted Customer instances in the session-level cache, so, when the transaction ends, 100 000 entities are managed by the persistence context. If the maximum memory allocated to the JVM is rather low, this example could fail with an OutOfMemoryException. The Java 1.8 JVM allocated either 1/4 of available RAM or 1Gb, which can easily accommodate 100 000 objects on the heap.

  2. long-running transactions can deplete a connection pool so other transactions don’t get a chance to proceed.

  3. JDBC batching is not enabled by default, so every insert statement requires a database roundtrip. To enable JDBC batching, set the hibernate.jdbc.batch_size property to an integer between 10 and 50.

Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.

12.2.1. Batch inserts

When you make new objects persistent, employ methods flush() and clear() to the session regularly, to control the size of the first-level cache.

Example 432. Flushing and clearing the Session

  1. EntityManager entityManager = null;
  2. EntityTransaction txn = null;
  3. try {
  4. entityManager = entityManagerFactory().createEntityManager();
  5. txn = entityManager.getTransaction();
  6. txn.begin();
  7. int batchSize = 25;
  8. for ( int i = 0; i < entityCount; i++ ) {
  9. if ( i > 0 && i % batchSize == 0 ) {
  10. //flush a batch of inserts and release memory
  11. entityManager.flush();
  12. entityManager.clear();
  13. }
  14. Person Person = new Person( String.format( "Person %d", i ) );
  15. entityManager.persist( Person );
  16. }
  17. txn.commit();
  18. } catch (RuntimeException e) {
  19. if ( txn != null && txn.isActive()) txn.rollback();
  20. throw e;
  21. } finally {
  22. if (entityManager != null) {
  23. entityManager.close();
  24. }
  25. }

12.2.2. Session scroll

When you retrieve and update data, flush() and clear() the session regularly. In addition, use method scroll() to take advantage of server-side cursors for queries that return many rows of data.

Example 433. Using scroll()

  1. EntityManager entityManager = null;
  2. EntityTransaction txn = null;
  3. ScrollableResults scrollableResults = null;
  4. try {
  5. entityManager = entityManagerFactory().createEntityManager();
  6. txn = entityManager.getTransaction();
  7. txn.begin();
  8. int batchSize = 25;
  9. Session session = entityManager.unwrap( Session.class );
  10. scrollableResults = session
  11. .createQuery( "select p from Person p" )
  12. .setCacheMode( CacheMode.IGNORE )
  13. .scroll( ScrollMode.FORWARD_ONLY );
  14. int count = 0;
  15. while ( scrollableResults.next() ) {
  16. Person Person = (Person) scrollableResults.get( 0 );
  17. processPerson(Person);
  18. if ( ++count % batchSize == 0 ) {
  19. //flush a batch of updates and release memory:
  20. entityManager.flush();
  21. entityManager.clear();
  22. }
  23. }
  24. txn.commit();
  25. } catch (RuntimeException e) {
  26. if ( txn != null && txn.isActive()) txn.rollback();
  27. throw e;
  28. } finally {
  29. if (scrollableResults != null) {
  30. scrollableResults.close();
  31. }
  32. if (entityManager != null) {
  33. entityManager.close();
  34. }
  35. }

If left unclosed by the application, Hibernate will automatically close the underlying resources (e.g. ResultSet and PreparedStatement) used internally by the ScrollableResults when the current transaction is ended (either commit or rollback).

However, it is good practice to close the ScrollableResults explicitly.

12.2.3. StatelessSession

StatelessSession is a command-oriented API provided by Hibernate. Use it to stream data to and from the database in the form of detached objects. A StatelessSession has no persistence context associated with it and does not provide many of the higher-level lifecycle semantics.

Some of the things not provided by a StatelessSession include:

  • a first-level cache

  • interaction with any second-level or query cache

  • transactional write-behind or automatic dirty checking

Limitations of StatelessSession:

  • Operations performed using a stateless session never cascade to associated instances.

  • Collections are ignored by a stateless session.

  • Lazy loading of associations is not supported.

  • Operations performed via a stateless session bypass Hibernate’s event model and interceptors.

  • Due to the lack of a first-level cache, Stateless sessions are vulnerable to data aliasing effects.

  • A stateless session is a lower-level abstraction that is much closer to the underlying JDBC.

Example 434. Using a StatelessSession

  1. StatelessSession statelessSession = null;
  2. Transaction txn = null;
  3. ScrollableResults scrollableResults = null;
  4. try {
  5. SessionFactory sessionFactory = entityManagerFactory().unwrap( SessionFactory.class );
  6. statelessSession = sessionFactory.openStatelessSession();
  7. txn = statelessSession.getTransaction();
  8. txn.begin();
  9. scrollableResults = statelessSession
  10. .createQuery( "select p from Person p" )
  11. .scroll(ScrollMode.FORWARD_ONLY);
  12. while ( scrollableResults.next() ) {
  13. Person Person = (Person) scrollableResults.get( 0 );
  14. processPerson(Person);
  15. statelessSession.update( Person );
  16. }
  17. txn.commit();
  18. } catch (RuntimeException e) {
  19. if ( txn != null && txn.getStatus() == TransactionStatus.ACTIVE) txn.rollback();
  20. throw e;
  21. } finally {
  22. if (scrollableResults != null) {
  23. scrollableResults.close();
  24. }
  25. if (statelessSession != null) {
  26. statelessSession.close();
  27. }
  28. }

The Customer instances returned by the query are immediately detached. They are never associated with any persistence context.

The insert(), update(), and delete() operations defined by the StatelessSession interface operate directly on database rows. They cause the corresponding SQL operations to be executed immediately. They have different semantics from the save(), saveOrUpdate(), and delete() operations defined by the Session interface.

12.3. Hibernate Query Language for DML

DML, or Data Manipulation Language, refers to SQL statements such as INSERT, UPDATE, and DELETE. Hibernate provides methods for bulk SQL-style DML statement execution, in the form of Hibernate Query Language (HQL).

12.3.1. HQL/JPQL for UPDATE and DELETE

Both the Hibernate native Query Language and JPQL (Java Persistence Query Language) provide support for bulk UPDATE and DELETE.

Example 435. Pseudo-syntax for UPDATE and DELETE statements using HQL

  1. UPDATE FROM EntityName e WHERE e.name = ?
  2. DELETE FROM EntityName e WHERE e.name = ?

Although the FROM and WHERE clauses are optional, it is good practice to declare them explicitly.

The FROM clause can only refer to a single entity, which can be aliased. If the entity name is aliased, any property references must be qualified using that alias. If the entity name is not aliased, then it is illegal for any property references to be qualified.

Joins, either implicit or explicit, are prohibited in a bulk HQL query. You can use sub-queries in the WHERE clause, and the sub-queries themselves can contain joins.

Example 436. Executing a JPQL UPDATE, using the Query.executeUpdate()

  1. int updatedEntities = entityManager.createQuery(
  2. "update Person p " +
  3. "set p.name = :newName " +
  4. "where p.name = :oldName" )
  5. .setParameter( "oldName", oldName )
  6. .setParameter( "newName", newName )
  7. .executeUpdate();

Example 437. Executing an HQL UPDATE, using the Query.executeUpdate()

  1. int updatedEntities = session.createQuery(
  2. "update Person " +
  3. "set name = :newName " +
  4. "where name = :oldName" )
  5. .setParameter( "oldName", oldName )
  6. .setParameter( "newName", newName )
  7. .executeUpdate();

In keeping with the EJB3 specification, HQL UPDATE statements, by default, do not effect the version or the timestamp property values for the affected entities. You can use a versioned update to force Hibernate to reset the version or timestamp property values, by adding the VERSIONED keyword after the UPDATE keyword.

Example 438. Updating the version of timestamp

  1. int updatedEntities = session.createQuery(
  2. "update versioned Person " +
  3. "set name = :newName " +
  4. "where name = :oldName" )
  5. .setParameter( "oldName", oldName )
  6. .setParameter( "newName", newName )
  7. .executeUpdate();

If you use the VERSIONED statement, you cannot use custom version types that implement the org.hibernate.usertype.UserVersionType.

This feature is only available in HQL since it’s not standardized by JPA.

Example 439. A JPQL DELETE statement

  1. int deletedEntities = entityManager.createQuery(
  2. "delete Person p " +
  3. "where p.name = :name" )
  4. .setParameter( "name", name )
  5. .executeUpdate();

Example 440. An HQL DELETE statement

  1. int deletedEntities = session.createQuery(
  2. "delete Person " +
  3. "where name = :name" )
  4. .setParameter( "name", name )
  5. .executeUpdate();

Method Query.executeUpdate() returns an int value, which indicates the number of entities affected by the operation. This may or may not correlate to the number of rows affected in the database. A JPQL/HQL bulk operation might result in multiple SQL statements being executed, such as for joined-subclass. In the example of joined-subclass, a DELETE against one of the subclasses may actually result in deletes in the tables underlying the join, or further down the inheritance hierarchy.

12.3.2. HQL syntax for INSERT

Example 441. Pseudo-syntax for INSERT statements

  1. INSERT INTO EntityName
  2. properties_list
  3. SELECT properties_list
  4. FROM ...

Only the INSERT INTO …​ SELECT …​ form is supported. You cannot specify explicit values to insert.

The properties_list is analogous to the column specification in the SQL INSERT statement. For entities involved in mapped inheritance, you can only use properties directly defined on that given class-level in the properties_list. Superclass properties are not allowed and subclass properties are irrelevant. In other words, INSERT statements are inherently non-polymorphic.

The SELECT statement can be any valid HQL select query, but the return types must match the types expected by the INSERT. Hibernate verifies the return types during query compilation, instead of expecting the database to check it. Problems might result from Hibernate types which are equivalent, rather than equal. One such example is a mismatch between a property defined as an org.hibernate.type.DateType and a property defined as an org.hibernate.type.TimestampType, even though the database may not make a distinction, or may be capable of handling the conversion.

If id property is not specified in the properties_list, Hibernate generates a value automatically. Automatic generation is only available if you use ID generators which operate on the database. Otherwise, Hibernate throws an exception during parsing. Available in-database generators are org.hibernate.id.SequenceGenerator and its subclasses, and objects which implement org.hibernate.id.PostInsertIdentifierGenerator.

For properties mapped as either version or timestamp, the insert statement gives you two options. You can either specify the property in the properties_list, in which case its value is taken from the corresponding select expressions or omit it from the properties_list, in which case the seed value defined by the org.hibernate.type.VersionType is used.

Example 442. HQL INSERT statement

  1. int insertedEntities = session.createQuery(
  2. "insert into Partner (id, name) " +
  3. "select p.id, p.name " +
  4. "from Person p ")
  5. .executeUpdate();

This section is only a brief overview of HQL. For more information, see HQL.

12.3.3. Bulk-id strategies

This article is about the HHH-11262 JIRA issue which now allows the bulk-id strategies to work even when you cannot create temporary tables.

Class diagram

Considering we have the following entities:

Entity class diagram

The Person entity is the base class of this entity inheritance model, and is mapped as follows:

Example 443. Bulk-id base class entity

  1. @Entity(name = "Person")
  2. @Inheritance(strategy = InheritanceType.JOINED)
  3. public static class Person implements Serializable {
  4. @Id
  5. private Integer id;
  6. @Id
  7. private String companyName;
  8. private String name;
  9. private boolean employed;
  10. //Getters and setters are omitted for brevity
  11. }

Both the Doctor and Engineer entity classes extend the Person base class:

Example 444. Bulk-id subclass entities

  1. @Entity(name = "Doctor")
  2. public static class Doctor extends Person {
  3. }
  4. @Entity(name = "Engineer")
  5. public static class Engineer extends Person {
  6. private boolean fellow;
  7. //Getters and setters are omitted for brevity
  8. }
Inheritance tree bulk processing

Now, when you try to execute a bulk entity delete query:

Example 445. Bulk-id delete query example

  1. int updateCount = session.createQuery(
  2. "delete from Person where employed = :employed" )
  3. .setParameter( "employed", false )
  4. .executeUpdate();
  1. create temporary table
  2. HT_Person
  3. (
  4. id int4 not null,
  5. companyName varchar(255) not null
  6. )
  7. insert
  8. into
  9. HT_Person
  10. select
  11. p.id as id,
  12. p.companyName as companyName
  13. from
  14. Person p
  15. where
  16. p.employed = ?
  17. delete
  18. from
  19. Engineer
  20. where
  21. (
  22. id, companyName
  23. ) IN (
  24. select
  25. id,
  26. companyName
  27. from
  28. HT_Person
  29. )
  30. delete
  31. from
  32. Doctor
  33. where
  34. (
  35. id, companyName
  36. ) IN (
  37. select
  38. id,
  39. companyName
  40. from
  41. HT_Person
  42. )
  43. delete
  44. from
  45. Person
  46. where
  47. (
  48. id, companyName
  49. ) IN (
  50. select
  51. id,
  52. companyName
  53. from
  54. HT_Person
  55. )

HT_Person is a temporary table that Hibernate creates to hold all the entity identifiers that are to be updated or deleted by the bulk id operation. The temporary table can be either global or local, depending on the underlying database capabilities.

Non-temporary table bulk-id strategies

As the HHH-11262 issue describes, there are use cases when the application developer cannot use temporary tables because the database user lacks this privilege.

In this case, we defined several options which you can choose depending on your database capabilities:

  • InlineIdsInClauseBulkIdStrategy

  • InlineIdsSubSelectValueListBulkIdStrategy

  • InlineIdsOrClauseBulkIdStrategy

  • CteValuesListBulkIdStrategy

InlineIdsInClauseBulkIdStrategy

To use this strategy, you need to configure the following configuration property:

  1. <property name="hibernate.hql.bulk_id_strategy"
  2. value="org.hibernate.hql.spi.id.inline.InlineIdsInClauseBulkIdStrategy"
  3. />

Now, when running the previous test case, Hibernate generates the following SQL statements:

Example 446. InlineIdsInClauseBulkIdStrategy delete entity query example

  1. select
  2. p.id as id,
  3. p.companyName as companyName
  4. from
  5. Person p
  6. where
  7. p.employed = ?
  8. delete
  9. from
  10. Engineer
  11. where
  12. ( id, companyName )
  13. in (
  14. ( 1,'Red Hat USA' ),
  15. ( 3,'Red Hat USA' ),
  16. ( 1,'Red Hat Europe' ),
  17. ( 3,'Red Hat Europe' )
  18. )
  19. delete
  20. from
  21. Doctor
  22. where
  23. ( id, companyName )
  24. in (
  25. ( 1,'Red Hat USA' ),
  26. ( 3,'Red Hat USA' ),
  27. ( 1,'Red Hat Europe' ),
  28. ( 3,'Red Hat Europe' )
  29. )
  30. delete
  31. from
  32. Person
  33. where
  34. ( id, companyName )
  35. in (
  36. ( 1,'Red Hat USA' ),
  37. ( 3,'Red Hat USA' ),
  38. ( 1,'Red Hat Europe' ),
  39. ( 3,'Red Hat Europe' )
  40. )

So, the entity identifiers are selected first and used for each particular update or delete statement.

The IN clause row value expression has long been supported by Oracle, PostgreSQL, and nowadays by MySQL 5.7. However, SQL Server 2014 does not support it, so you’ll have to use a different strategy.

InlineIdsSubSelectValueListBulkIdStrategy

To use this strategy, you need to configure the following configuration property:

  1. <property name="hibernate.hql.bulk_id_strategy"
  2. value="org.hibernate.hql.spi.id.inline.InlineIdsSubSelectValueListBulkIdStrategy"
  3. />

Now, when running the previous test case, Hibernate generates the following SQL statements:

Example 447. InlineIdsSubSelectValueListBulkIdStrategy delete entity query example

  1. select
  2. p.id as id,
  3. p.companyName as companyName
  4. from
  5. Person p
  6. where
  7. p.employed = ?
  8. delete
  9. from
  10. Engineer
  11. where
  12. ( id, companyName ) in (
  13. select
  14. id,
  15. companyName
  16. from (
  17. values
  18. ( 1,'Red Hat USA' ),
  19. ( 3,'Red Hat USA' ),
  20. ( 1,'Red Hat Europe' ),
  21. ( 3,'Red Hat Europe' )
  22. ) as HT
  23. (id, companyName)
  24. )
  25. delete
  26. from
  27. Doctor
  28. where
  29. ( id, companyName ) in (
  30. select
  31. id,
  32. companyName
  33. from (
  34. values
  35. ( 1,'Red Hat USA' ),
  36. ( 3,'Red Hat USA' ),
  37. ( 1,'Red Hat Europe' ),
  38. ( 3,'Red Hat Europe' )
  39. ) as HT
  40. (id, companyName)
  41. )
  42. delete
  43. from
  44. Person
  45. where
  46. ( id, companyName ) in (
  47. select
  48. id,
  49. companyName
  50. from (
  51. values
  52. ( 1,'Red Hat USA' ),
  53. ( 3,'Red Hat USA' ),
  54. ( 1,'Red Hat Europe' ),
  55. ( 3,'Red Hat Europe' )
  56. ) as HT
  57. (id, companyName)
  58. )

The underlying database must support the VALUES list clause, like PostgreSQL or SQL Server 2008. However, this strategy requires the IN-clause row value expression for composite identifiers, and for this reason, you can only use the InlineIdsSubSelectValueListBulkIdStrategy strategy with PostgreSQL.

InlineIdsOrClauseBulkIdStrategy

To use this strategy, you need to configure the following configuration property:

  1. <property name="hibernate.hql.bulk_id_strategy"
  2. value="org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy"
  3. />

Now, when running the previous test case, Hibernate generates the following SQL statements:

Example 448. InlineIdsOrClauseBulkIdStrategy delete entity query example

  1. select
  2. p.id as id,
  3. p.companyName as companyName
  4. from
  5. Person p
  6. where
  7. p.employed = ?
  8. delete
  9. from
  10. Engineer
  11. where
  12. ( id = 1 and companyName = 'Red Hat USA' )
  13. or ( id = 3 and companyName = 'Red Hat USA' )
  14. or ( id = 1 and companyName = 'Red Hat Europe' )
  15. or ( id = 3 and companyName = 'Red Hat Europe' )
  16. delete
  17. from
  18. Doctor
  19. where
  20. ( id = 1 and companyName = 'Red Hat USA' )
  21. or ( id = 3 and companyName = 'Red Hat USA' )
  22. or ( id = 1 and companyName = 'Red Hat Europe' )
  23. or ( id = 3 and companyName = 'Red Hat Europe' )
  24. delete
  25. from
  26. Person
  27. where
  28. ( id = 1 and companyName = 'Red Hat USA' )
  29. or ( id = 3 and companyName = 'Red Hat USA' )
  30. or ( id = 1 and companyName = 'Red Hat Europe' )
  31. or ( id = 3 and companyName = 'Red Hat Europe' )

The InlineIdsOrClauseBulkIdStrategy strategy has the advantage of being supported by all the major relational database systems (e.g. Oracle, SQL Server, MySQL, and PostgreSQL).

CteValuesListBulkIdStrategy

To use this strategy, you need to configure the following configuration property:

  1. <property name="hibernate.hql.bulk_id_strategy"
  2. value="org.hibernate.hql.spi.id.inline.CteValuesListBulkIdStrategy"
  3. />

Now, when running the previous test case, Hibernate generates the following SQL statements:

Example 449. CteValuesListBulkIdStrategy delete entity query example

  1. select
  2. p.id as id,
  3. p.companyName as companyName
  4. from
  5. Person p
  6. where
  7. p.employed = ?
  8. with HT_Person (id,companyName ) as (
  9. select id, companyName
  10. from (
  11. values
  12. (?, ?),
  13. (?, ?),
  14. (?, ?),
  15. (?, ?)
  16. ) as HT (id, companyName) )
  17. delete
  18. from
  19. Engineer
  20. where
  21. ( id, companyName ) in (
  22. select
  23. id, companyName
  24. from
  25. HT_Person
  26. )
  27. with HT_Person (id,companyName ) as (
  28. select id, companyName
  29. from (
  30. values
  31. (?, ?),
  32. (?, ?),
  33. (?, ?),
  34. (?, ?)
  35. ) as HT (id, companyName) )
  36. delete
  37. from
  38. Doctor
  39. where
  40. ( id, companyName ) in (
  41. select
  42. id, companyName
  43. from
  44. HT_Person
  45. )
  46. with HT_Person (id,companyName ) as (
  47. select id, companyName
  48. from (
  49. values
  50. (?, ?),
  51. (?, ?),
  52. (?, ?),
  53. (?, ?)
  54. ) as HT (id, companyName) )
  55. delete
  56. from
  57. Person
  58. where
  59. ( id, companyName ) in (
  60. select
  61. id, companyName
  62. from
  63. HT_Person
  64. )

The underlying database must support CTE (Common Table Expressions) that can be referenced from non-query statements as well. For instance, PostgreSQL supports this feature since version 9.1 and SQL Server offers support for it since version 2005.

The underlying database must also support the VALUES list clause, like PostgreSQL or SQL Server 2008.

However, this strategy requires the IN-clause row value expression for composite identifiers, so you can only use this strategy with PostgreSQL.

If you can use temporary tables, that’s probably the best choice. However, if you are not allowed to create temporary tables, you must pick one of these four strategies that works with your underlying database. Before making up your mind, you should benchmark which one works best for your current workload. For instance, CTE are optimization fences in PostgreSQL, so make sure you measure before making a decision.

If you’re using Oracle or MySQL 5.7, you can choose either InlineIdsOrClauseBulkIdStrategy or InlineIdsInClauseBulkIdStrategy. For older version of MySQL, then you can only use InlineIdsOrClauseBulkIdStrategy.

If you’re using SQL Server, InlineIdsOrClauseBulkIdStrategy is the only option for you.

If you’re using PostgreSQL, then you have the luxury of choosing any of these four strategies.