Spring read-only transaction Hibernate optimization
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, I’m going to explain how the Spring read-only transaction Hibernate optimization works.
After taking a look at what the Spring framework does when enabling the readOnly
attribute on the @Transactional
annotation, I realized that only the Hibernate flush mode is set to FlushType.MANUAL
without propagating the read-only flag further to the Hibernate Session
.
So, in the true spirit of open-source software developer, I decided it’s time to make a change.
Today is a good day to contribute a #Hibernate optimization to @springframework
— Vlad Mihalcea (@vlad_mihalcea) June 19, 2018
How the Spring read-only transaction #Hibernate optimization works. @vlad_mihalcea https://t.co/ff5qo2Znoo pic.twitter.com/S3Od6JiD7d
— Java (@java) September 29, 2018
Entity loaded state
When loading an entity, Hibernate extracts the loaded state from the underlying JDBC ResultSet
. This process is called hydration on Hibernate terminology and is done by the Hibernate EntityPersister
like this:
final Object[] values = persister.hydrate( rs, id, object, rootPersister, cols, fetchAllPropertiesRequested, session );
The loaded state or hydrated state is needed by dirty checking mechanism to compare the current entity state with the loaded-time snapshot and determine if an UPDATE
statement is needed to be executed at flush-time. Also, the loaded state is used by the versionless optimistic locking mechanism to build the WHERE clause filtering predicates.
Therefore, upon loading an entity, the loaded state is stored by the Hibernate Session
unless the entity is loaded in read-only mode.
Read-only entities
By default, entities are loaded in read-write mode, meaning that the loaded state is kept by the current Persistence Context until the entity is loaded or if the JPA EntityManager
or Hibernate Session
is closed.
In order to load entities in the read-only mode, you can set either set the defaultReadOnly
flag at the Session
level or set the org.hibernate.readOnly
JPA query hint.
To set the read-only for all entities loaded by a Hibernate Session
either through a query or via direct fetching, you need to enable the defaultReadOnly
property like this:
Session session = entityManager.unwrap(Session.class); session.setDefaultReadOnly(true);
Or, if you have a default read-write Session
and only want to load entities in read-only
mode for a particular query, you can use the org.hibernate.readOnly
JPA query hint as follows:
List<Post> posts = entityManager .createQuery( "select p from Post p", Post.class) .setHint(QueryHints.HINT_READONLY, true) .getResultList();
Spring @Transactional annotation
Spring, just like Java EE, offers support for declarative transactions. Therefore, you can use the @Transactional
annotation to mark the service layer method that should be wrapped in a transactional context.
The @Transactional
annotation offers the readOnly
attribute, which is false
by default. The readOnly
attribute can further be used by Spring to optimize the underlying data access layer operations.
Prior to Spring 5.1, when using Hibernate, the readOnly
attribute of the @Transactional
annotation was only setting the current Session
flush mode to FlushType.MANUAL
, therefore disabling the automatic dirty checking mechanism.
However, because the readOnly
attribute did not propagate to the underlying Hibernate Session
, I decided to create the SPR-16956 issue and provided a Pull Request as well, which after being Jürgenized, it got integrated, and available starting with Spring Framework 5.1.
Testing time
Let’s consider we have the following service and data access layer classes in our application:
The PostDAOImpl
class is implemented like this:
@Repository public class PostDAOImpl extends GenericDAOImpl<Post, Long> implements PostDAO { protected PostDAOImpl() { super(Post.class); } @Override public List<Post> findByTitle(String title) { return getEntityManager() .createQuery( "select p " + "from Post p " + "where p.title = :title", Post.class) .setParameter("title", title) .getResultList(); } }
While the ForumServiceImpl
looks as follows:
@Service public class ForumServiceImpl implements ForumService { @Autowired private PostDAO postDAO; @Autowired private TagDAO tagDAO; @PersistenceContext private EntityManager entityManager; @Override @Transactional public Post newPost(String title, String... tags) { Post post = new Post(); post.setTitle(title); post.getTags().addAll(tagDAO.findByName(tags)); return postDAO.persist(post); } @Override @Transactional(readOnly = true) public List<Post> findAllByTitle(String title) { List<Post> posts = postDAO.findByTitle(title); org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); for(Post post : posts) { assertTrue(entityManager.contains(post)); EntityEntry entityEntry = persistenceContext.getEntry(post); assertNull(entityEntry.getLoadedState()); } return posts; } @Override @Transactional public Post findById(Long id) { Post post = postDAO.findById(id); org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext(); EntityEntry entityEntry = persistenceContext.getEntry(post); assertNotNull(entityEntry.getLoadedState()); return post; } private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext() { SharedSessionContractImplementor session = entityManager.unwrap( SharedSessionContractImplementor.class ); return session.getPersistenceContext(); } }
We are interested in the findAllByTitle
and findById
service methods.
Notice that the findAllByTitle
method is annotated with @Transactional(readOnly = true)
. When loading the Post
entities matching the given title
, Hibernate is going to fetch the entities in read-only mode, therefore discarding the loaded state, which we can validate via the Hibernate PersistenceContext
.
On the other hand, the findById
method uses the default read-write @Transactional
annotation, and we can see that Hibernate PersistenceContext
contains the loaded state of the currently fetched Post
entity.
When running the test that proves this new Spring 5.1 Hibernate optimization, everything works as expected:
@Test public void test() { Post newPost = forumService.newPost( "High-Performance Java Persistence", "hibernate", "jpa" ); assertNotNull(newPost.getId()); List<Post> posts = forumService.findAllByTitle( "High-Performance Java Persistence" ); assertEquals(1, posts.size()); Post post = forumService.findById(newPost.getId()); assertEquals( "High-Performance Java Persistence", post.getTitle() ); }
Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The main advantage of the Spring 5.1 read-only optimization for Hibernate is that we can save a lot of memory when loading read-only entities since the loaded state is discarded right away, and not kept for the whole duration of the currently running Persistence Context.
