Spring read-only transaction Hibernate optimization

Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

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.

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 detached state is used by the versionless optimistic locking mechanism to build the WHERE clause filtering predicates.

Therefore, upon loading an entity, the detached 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 detached state is kept by the current Persistence Context until the entity is detached or if the JPA EntityManager or Hibernate Session is closed.

In order to load entities is 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:

Forum service and data access layer classes

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 detached 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.

FREE EBOOK

20 Comments on “Spring read-only transaction Hibernate optimization

  1. Is the topic “Generic DAO” and its implementation (e.g. source code) also covered in your book High-Performance Java Persistance?

    • The Generic DAO can be found in my High-Performance Java Persistence GitHub repository. The book will teach you how to optimize your data access code.

  2. Hi, Vlad!
    As I understood, if I set readOnly=true, enable SQL logging and call entityManager.find(id) three times, I’ll see three SELECT statements for that entity from database. Is that correct?

    • That’s not what read-only is for. Calling find 3 times may result in 0, 1 or 3 queries, depending on your Persistence Context lifecycle. Check out my High-Performance Java Persistence book for more details.

  3. Is this optimization’s effect just in persistence context and application side’s memory ? I tested a sample , a findAll query on a table with 1000 rows in mysql with readonly true/false, by setting hibernate.generate_statistics=true
    and I found with readonly=true the spent executing JDBC statements is 20% less than with readonly=false, is there optimization’s impact on database side ? or this is just result of optimization in persistence context?

    • This setting has no effect on the DB side. You need to use JMH if you want to get conclusive test results which use a very large number of iterations with a warming up phase. Otherwise, one time checks are not very useful to draw conclusions about performance improvements.

      • Hi, Vlad. Have you done, or maybe come across, such a JMH test for cases with @Transactional(readOnly=”true”) and without such annotation? I believe sharing a good benchmark output would complement this post greatly

      • I will publish one benchmark in the second edition of the High-Performance Java Persistence book.

  4. If I am using Spring Data. I using SimpleJpaRepository, which is annotated with @Transactional(readOnly=true). So how hibernate without saving hydrated object state after loading by repository can check it for update after in my service annotated with @Transactional. This means, than if I seat readonly and then transactional service is called, read only flag is removed?

    • The outermost transaction defines the transaction settings. So, if it’s read-only, it does not matter whether you will call a read-write service, it will still reuse the same read-only Session.

  5. With Spring Boot 2x, JpaTransactionManager is auto configured, so changes to HibernateTransactionManager has no effects to new Spring Boot apps?

    • That’s right. Spring Boot uses the JpaTransactionManager by default, so this optimization is applied to it just fine. However, if use HibernateTransactionManager with a classis Spring application, the optimiztaion should also be applied.

      • But in Pull Request I see changes only to HibernateTransactionManager not JpaTransactionManager. How JpaTransactionManager will set hibernate session to read only?

      • That’s not the PR that got integrated. It was just a proof of concept, and it did not require any change to the JpaTransactionManager as the HibernateJpaDialect change request was already covering that part.

  6. Is it mandatory to declare @transactional in the service and Repository both the place

    • The @Transactional annotation should be placed on the service layer, not the Repository level. The service layer defines the transactional boundaries which can span over multiple repository calls.

  7. Can you detail on how the @javax.transaction.transactional annotation is different from spring transactional annotation and when and how to use javax variant in the application?

    • If you are using Java EE, use javax. If you’re using Spring, use the Spring one which is more powerful. It’s as simple as that.

  8. I would expect that any write performed during read-only transaction will be rejected and signaled by exception. As I see only in case of new entity creation such exception is thrown. No exceptions for removal and updates. What are your thoughts about it?

    • Read-only means that the entities are loaded in read-only mode, hence no update will be triggered. As for deletes, read-only entity, like immutable objects, are allowed to be removed. If you think this functionality is not consistent, you should open a Jira issue for Hibernate.

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.