javahibernatequarkusquarkus-panache

Quarkus Hibernate ORM Panache - batching inserts not working


Ive been running some volume tests on an application recently, and I noticed that the application is way to slow during high volume transactions. Doing some debugging, I noticed that the the inserts are not being batched. I ran into the same problem building a minimal reproducer from scratch.

Ive also tried using the EntityManager directly and not use any Panache, but had the same result.

TestEntity.java

package org.example;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.UUID;

@Entity
@Table(name = "TestEntity", schema = "testdb")
public class TestEntity extends PanacheEntityBase {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "id")
    private UUID id;

    @Column(name = "testCol")
    private final Integer testCol;

    private TestEntity(final Builder builder) {
        this.testCol = builder.testCol;
    }

    protected TestEntity() {
        this.id = null;
        this.testCol = null;
    }

    public UUID getId() {
        return id;
    }

    public Integer getTestCol() {
        return testCol;
    }

    public static final class Builder {
        private Integer testCol;

        public Builder testCol(final Integer testCol) {
            this.testCol = testCol;
            return this;
        }

        public TestEntity build() {
            return new TestEntity(this);
        }
    }
}

TestResource.java

package org.example;

import jakarta.transaction.Transactional;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;

import java.util.ArrayList;
import java.util.List;

@Path("/test")
public class TestResource {

    @POST
    @Transactional
    public Response hello() {
        try {

            List<TestEntity> testEntities = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                testEntities.add(new TestEntity.Builder().testCol(i).build());
            }

            TestEntityRepository testEntityRepository = new TestEntityRepository();

            testEntityRepository.persist(testEntities);

            return Response.ok().build();
        } catch (Exception e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }

    }
}

TestEntityRepository.java

package org.example;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

import java.util.Optional;
import java.util.UUID;

@ApplicationScoped
public class TestEntityRepository implements PanacheRepository<TestEntity> {

public Optional<TestEntity> findById(final UUID id) {
    return find("id", id).firstResultOptional();
  }
}

In the quarkus console, I get multiple identical insert statements, clearly showing that there is no batching.

Quarkus Console

2025-07-02 15:20:59,929 DEBUG [org.hib.eng.jdb.spi.SqlStatementLogger] (executor-thread-2)
    insert 
    into
        testdb.TestEntity
        (testCol, id) 
    values
        (?, ?)
2025-07-02 15:20:59,930 DEBUG [org.hib.eng.jdb.spi.SqlStatementLogger] (executor-thread-2)
    insert 
    into
        testdb.TestEntity
        (testCol, id) 
    values
        (?, ?)
 2025-07-02 15:20:59,930 DEBUG [org.hib.cac.int.TimestampsCacheEnabledImpl] (executor-thread-2) Pre-invalidating space [testdb.TestEntity], timestamp: 1751462519930
 2025-07-02 15:20:59,932 DEBUG [org.hib.eng.jdb.bat.int.BatchImpl] (executor-thread-2) PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : insert into testdb.TestEntity (testCol,id) values (?,?)
 2025-07-02 15:20:59,933 DEBUG [org.hib.eng.tra.int.TransactionImpl] (executor-thread-2) On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
 2025-07-02 15:20:59,933 DEBUG [org.hib.res.jdb.int.LogicalConnectionManagedImpl] (executor-thread-2) Initiating JDBC connection release from beforeTransactionCompletion
 2025-07-02 15:20:59,933 DEBUG [org.hib.eng.jdb.bat.int.BatchImpl] (executor-thread-2) PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : insert into testdb.TestEntity (testCol,id) values (?,?)
 2025-07-02 15:20:59,946 FINE [org.pos.jdb.PgConnection] (executor-thread-2) setAutoCommit = true
 2025-07-02 15:20:59,947 DEBUG [org.hib.eng.jdb.bat.int.BatchImpl] (executor-thread-2) PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : insert into testdb.TestEntity (testCol,id) values (?,?)
 2025-07-02 15:20:59,947 DEBUG [org.hib.res.jdb.int.LogicalConnectionManagedImpl] (executor-thread-2) Initiating JDBC connection release from afterTransaction
 2025-07-02 15:20:59,947 DEBUG [org.hib.cac.int.TimestampsCacheEnabledImpl] (executor-thread-2) Invalidating space [testdb.TestEntity], timestamp: 1751462459947
 2025-07-02 15:20:59,948 DEBUG [org.hib.eng.jdb.int.JdbcCoordinatorImpl] (executor-thread-2) HHH000420: Closing un-released batch
 2025-07-02 15:20:59,948 DEBUG [org.hib.eng.jdb.bat.int.BatchImpl] (executor-thread-2) PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : insert into testdb.TestEntity (testCol,id) values (?,?)
 2025-07-02 15:20:59,948 DEBUG [org.hib.eng.jdb.bat.int.BatchImpl] (executor-thread-2) PreparedStatementDetails did not contain PreparedStatement on #releaseStatements : insert into testdb.TestEntity (testCol,id) values (?,?)

Here are some application.properties that might be of interest. application.properties

quarkus.datasource.jdbc.transactions=enabled
quarkus.transaction-manager.default-transaction-timeout=6000 #high timeout for testing purposes
quarkus.hibernate-orm.jdbc.statement-batch-size=1000
quarkus.hibernate-orm.unsupported-properties."hibernate.order_inserts" = true

Solution

  • In the quarkus console, I get multiple identical insert statements, clearly showing that there is no batching.

    You're assuming batching means merging multiple insert statements into one. It does not.

    First, let's clarify that quarkus.hibernate-orm.jdbc.statement-batch-size is about batching statements in fewer network packets. You'll still have multiple insert statements, it's just that the JDBC driver will try its best to send them in one network packet ("batch"), avoiding latency issues: for two statements, if you trigger one DB round-trip (request/response), you'd get half the latency you would have gotten with two round-trips. This is what gets you better performance.

    Second, this:

    quarkus.hibernate-orm.unsupported-properties."hibernate.order_inserts" = true
    

    ... is obviously unsupported (from the name of the property), though it might still work in most cases. That being said, it's not about merging statements either. It's about executing statements in an specific order, which will help batch them, since multiple identical statements will be more likely to happen in succession.

    None of what you're using here is supposed to merge multiple insert statements together.

    But that's fine! Once the statements reach the database, INSERT INTO ... VALUES (...), (...); (what you were hoping for) should not make much of a difference compared to INSERT INTO ... VALUES (...); INSERT INTO ... VALUES (...); (what you're getting) in terms of performance -- at least, if these statements are batched in the same network packets.

    If you want to know more, you might be interested in this (old) conversation: https://hibernate.zulipchat.com/#narrow/channel/132096-hibernate-user/topic/Hibernate.20batching.20performance.20to.20network.20DB/with/449107284 Someone tried to achieve this statement merging by using features that are specific to the MariaDB/MySQL JDBC drivers, and confirmed that, no, merging statements doesn't help performance.