• Ensure your JDBC driver supports batching and that it’s enabled in your database connection URL (e.g., for MySQL, add the rewriteBatchedStatements=true parameter).

application.yml:

spring:
  datasource:
    url: jdbc:postgresql://<host>:<port>/<db_name>?rewriteBatchedInserts=true
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 25
          batch_versioned_data: true # For @Version optimistic locking
        order_inserts: true
        order_updates: true


  • Logging configuration for batching verification during development. Not recommended for production.

Recipe to configure logging for Spring Data JPA, Hibernate

Recipe to expose Hibernate Statistics through JMX MBeans in Spring Data JPA

application.yml:

logging:
  level:
#    org.hibernate.engine.jdbc.batch.internal.BatchingBatch # Hibernate 5.x
    org.hibernate.orm.jdbc.batch: TRACE # Hibernate 6.x
    org.hibernate.stat: DEBUG

spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true


  • Avoid using IDENTITY in JPA Entities’ primary keys meant to be inserted, either parent or children associations, because Hibernate must retrieve the ID immediately after each insert.

Product.java:

@Entity
// ...
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_id_seq")
    @SequenceGenerator(
        name = "product_id_seq",
        sequenceName = "product_id_sequence",
        // Match allocationSize with batch_size from application.yml config for efficiency
        // Match allocationSize with product_id_seq sequence's CACHE value
        allocationSize = 25
    )
    private Long id;
// ...
}


schema.sql:

CREATE SEQUENCE product_id_seq
    INCREMENT BY 1
    NO MAXVALUE
    NO MINVALUE
    CACHE 25;  -- Make JPA Entity's @SequenceGenerator.allocationSize attribute match this value


Usage

  • A Batch job to process active customers, update some attributes then disactivating them to prevent an infinite loop.

DefaultDvdRentalService.java:

@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void processActiveCustomers() {
    boolean stillPending = true;
    while(stillPending) {
        stillPending = this.rwTransactionTemplate.execute(txStatus -> {
            Pageable page = PageRequest.of(0, 25);
            List<Customer> customers = this.customerRepository.findAndLockTopNByActiveOrderByLastUpdateAsc(page, 1);
            if (CollectionUtils.isEmpty(customers)) {
                return false;
            }

            // Entities to process
            customers.forEach((customer) -> {
                // Implement some logic
                // FIXME

                // Update the attribute used in the select statement
                // so that the loop doesn't become an infinite loop
                customer.setActive(0);
                customer.setActivebool(false);
            });

            return true;
        });
    }
}


  • Verification logs.
...
2025-10-26T17:29:30.732-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Created Batch (25) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
2025-10-26T17:29:30.732-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Adding to JDBC batch (1) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
2025-10-26T17:29:30.732-04:00 DEBUG 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.SQL                        : 
    update
        public.customer 
    set
        active=?,
        activebool=?,
        address_id=?,
        create_date=?,
        email=?,
        first_name=?,
        last_name=?,
        last_update=?,
        store_id=? 
    where
        customer_id=?
...
2025-10-26T17:29:30.679-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Adding to JDBC batch (2) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
2025-10-26T17:29:30.679-04:00 DEBUG 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.SQL                        : 
    update
        public.customer 
    set
        active=?,
        activebool=?,
        address_id=?,
        create_date=?,
        email=?,
        first_name=?,
        last_name=?,
        last_update=?,
        store_id=? 
    where
        customer_id=?
...
2025-10-26T17:29:30.679-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Adding to JDBC batch (3) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
...
2025-10-26T17:29:30.699-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Adding to JDBC batch (25) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
...
2025-10-26T17:29:30.708-04:00 TRACE 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : Executing JDBC batch (25 / 25) - `com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`
2025-10-26T17:29:30.713-04:00 DEBUG 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : update public.customer set active=?,activebool=?,address_id=?,create_date=?,email=?,first_name=?,last_name=?,last_update=?,store_id=? where customer_id=?
2025-10-26T17:29:30.717-04:00 DEBUG 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] org.hibernate.orm.jdbc.batch             : PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : update public.customer set active=?,activebool=?,address_id=?,create_date=?,email=?,first_name=?,last_name=?,last_update=?,store_id=? where customer_id=?
2025-10-26T17:29:30.721-04:00  INFO 31441 --- [spring-boot-scheduled-multiple-threads] [eduler-thread-1] i.StatisticalLoggingSessionEventListener : Session Metrics {
    25163 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    87070 nanoseconds spent preparing 2 JDBC statements;
    2232062 nanoseconds spent executing 1 JDBC statements;
    4334130 nanoseconds spent executing 1 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    36783018 nanoseconds spent executing 1 flushes (flushing a total of 25 entities and 50 collections);
    4743 nanoseconds spent executing 1 pre-partial-flushes;
    2817 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}


Relevant logs:

  • Executing JDBC batch (25 / 25) - com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`

  • 87070 nanoseconds spent preparing 2 JDBC statements;
    One for the select ... fetch first ? rows only for no key update skip locked, another for the update public.customer set ...

  • 4334130 nanoseconds spent executing 1 JDBC batches;
    References the UPDATE 25 rows statement Executing JDBC batch (25 / 25) - com.asimiotech.demo.domain.dvdrental.Customer#UPDATE`