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.