Hibernate StatelessSession JDBC Batching

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see how we can use the Hibernate StatelessSession in order to enable JDBC Batching for INSERT, UPDATE, and DELETE statements.

While the StatelessSession has been available for more than 20 years, until Hibernate 6, its usefulness was rather limited since it lacked support for batching, as well as other cool features, such as UPSERT.

Hibernate StatelessSession

Just like the Hibernate Session, you can create a Hibernate StatelessSession from the Hibernate SessionFactory:

StatelessSession session = sessionFactory.withStatelessOptions().openStatelessSession();

However, unlike the Hibernate Session or JPA EntityManager, the Hibernate StatelessSession doesn’t have an associated Persistence Context. For this reason, there is no First-Level Cache, and for this reason, entity operation cascading or dirty checking are not going to work when you are using the StatelessSession.

To simplify our integration tests that will demonstrate the Hibernate StatelessSession support for JDBC Batching, I’m going to create the following doInStatelessSession method that executes a Java Lambda in the context of a newly-created StatelessSession and JPA transaction:

protected void doInStatelessSession(
        HibernateStatelessTransactionConsumer callable) {
    StatelessSession session = null;
    Transaction transaction = null;
 
    try {
        session = sessionFactory()
            .withStatelessOptions()
            .openStatelessSession();
 
        transaction = session.beginTransaction();
 
        callable.accept(session);
 
        if (!transaction.getRollbackOnly()) {
            transaction.commit();
        } else {
            try {
                transaction.rollback();
            } catch (Exception e) {
                LOGGER.error("Rollback failure", e);
            }
        }
    } catch (Throwable t) {
        if (transaction != null && transaction.isActive()) {
            try {
                transaction.rollback();
            } catch (Exception e) {
                LOGGER.error("Rollback failure", e);
            }
        }
        throw t;
    } finally {
        if (session != null) {
            session.close();
        }
    }
}

Just like typical JPA EntityManager or Hibernate Session, which simply extends the JPA EntityManager, the Hibernate StatelessSession can take advantage of the automatic JDBC batching feature of Hibernate that you can enable via the following settings:

properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");

Batching INSERT statements with the Hibernate StatelessSession

Now, when inserting 3 Post entities using the doInStatelessSession utility method:

doInStatelessSession(session -> {
    for (long i = 1; i <= POST_COUNT; i++) {
        session.insert(
            new Post()
                .setId(i)
                .setTitle(String.format("Post no. %d", i))
        );
    }
});

Hibernate is going to execute the following SQL INSERT statements:

Query:["
    insert into post (title,id) values (?,?)
"], 
Params:[
    (Post no. 1, 1), 
    (Post no. 2, 2), 
    (Post no. 3, 3)
]

And the batching is not limited to inserting a single entity type. While cascading is not supported because the StatelessSession doesn’t have a Persistence Context, you can still insert parent and child entities and benefit from JDBC batch inserts, as illustrated by the following test case:

doInStatelessSession(session -> {
    List<Post> posts = LongStream.rangeClosed(1, POST_COUNT).boxed()
        .map(i -> {
            Post post = new Post()
                .setId(i)
                .setTitle(
                    String.format(
                        "Post no. %d", 
                        i
                    )
                );
 
            session.insert(post);
 
            return post;
        })
        .toList();
 
    final int COMMENT_COUNT = 5;
 
    posts.forEach(post -> {
        for (long i = 1; i <= COMMENT_COUNT; i++) {
            session.insert(
                new PostComment()
                    .setPost(post)
                    .setReview(
                        String.format(
                            "Post comment no. %d",
                            (post.getId() - 1) * COMMENT_COUNT + i
                        )
                    )
            );
        }
    });
});

When executing the above integration test, Hibernate executes the following SQL INSERT statements:

Query:["
    insert into post (title,id) values (?,?)
"], 
Params:[
    (Post no. 1, 1), 
    (Post no. 2, 2), 
    (Post no. 3, 3)
]
Query:["
    insert into post_comment (post_id,review,id) values (?,?,?)
"], 
Params:[
    (1, Post comment no. 1, 1),
    (1, Post comment no. 2, 2),
    (1, Post comment no. 3, 3),
    (1, Post comment no. 4, 4),
    (1, Post comment no. 5, 5),
    (2, Post comment no. 6, 6),
    (2, Post comment no. 7, 7),
    (2, Post comment no. 8, 8),
    (2, Post comment no. 9, 9),
    (2, Post comment no. 10, 10),
    (3, Post comment no. 11, 11),
    (3, Post comment no. 12, 12),
    (3, Post comment no. 13, 13),
    (3, Post comment no. 14, 14),
    (3, Post comment no. 15, 15)
]

Batching UPDATE statements with the Hibernate StatelessSession

Now, let’s assume we have loaded several Post entities and modified them while the entities are in the detached state:

List<Post> posts = doInJPA(entityManager -> {
    return entityManager.createQuery("""
        select p
        from Post p
        """, Post.class)
    .setMaxResults(POST_COUNT)
    .getResultList();
});
 
posts.forEach(post ->
    post.setTitle(
        post.getTitle().replaceAll("no", "nr")
    )
);

To save the changes back to the database, we can use the update method of the Hibernate StatelessSession:

doInStatelessSession(session -> {
    posts.forEach(session::update);
});

When executing the update method calls, we can see that Hibernate executes a single batch UPDATE statement:

Query:["
    update post set title=? where id=?
"], 
Params:[
    (Post nr. 1, 1),
    (Post nr. 2, 2),
    (Post nr. 3, 3)
]

Batching DELETE statements with the Hibernate StatelessSession

Let’s assume we have a list of Post entities that we need to delete. Because the Hibernate StatelessSession doesn’t support cascading, we will have to delete the child entities using a Bulk Delete Query prior to deleting the parent entities, as illustrated by the following example:

doInStatelessSession(session -> {
    HibernateCriteriaBuilder builder = session.getCriteriaBuilder();
 
    JpaCriteriaDelete<PostComment> criteria = builder
        .createCriteriaDelete(PostComment.class);
    Root<PostComment> post = criteria.from(PostComment.class);
 
    session.createQuery(
        criteria
            .where(builder.in(post.get("post"), posts))
    )
    .executeUpdate();
  
    posts.forEach(session::delete);
});

When running the above integration test, Hibernate is going to execute the following DELETE statements:

Query:["
    delete from post_comment where post_id in (?,?,?)
"], 
Params:[
    (1, 2, 3)
]
Query:[
    "delete from post where id=?
"], Params:[
    (1),
    (2),
    (3)
]

The first statement is the Bulk Delete that removes the associated post_comment table records, and the second statement is the batched DELETE statement that removes the post entries.

That’s it!

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As I explained in this article, if we need to update some detached entities, we can either use the JPA EntityManager merge method or the Hibernate Session update method.

The merge method will execute an unnecessary SELECT statement, and that’s an overhead to a batch processing task whose only goal is to update the entities it has changed according to a given business logic.

While the update method of the Hibernate Session forces the UPDATE statement without doing an extra SELECT, many developers will avoid this method since it’s been deprecated. In my opinion, the Session update method served a good purpose, and that’s why I opened the HHH-15915 issue to remove the deprecated flag.

In the meanwhile, if you have some entities in the detached state that need to be updated, you can use the StatelessSession update method as an alternative to the JPA merge method and avoid executing some extra SELECT statements.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.