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!


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(

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);

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
    "select p from Post p", Post.class)
.setHint(QueryHints.HINT_READONLY, true)

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:

public class PostDAOImpl 
        extends GenericDAOImpl<Post, Long> 
        implements PostDAO {

    protected PostDAOImpl() {

    public List<Post> findByTitle(String title) {
        return getEntityManager()
            "select p " +
            "from Post p " +
            "where p.title = :title", Post.class)
        .setParameter("title", title)

While the ForumServiceImpl looks as follows:

public class ForumServiceImpl implements ForumService {

    private PostDAO postDAO;

    private TagDAO tagDAO;

    private EntityManager entityManager;

    public Post newPost(String title, String... tags) {
        Post post = new Post();
        return postDAO.persist(post);

    @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) {

            EntityEntry entityEntry = persistenceContext.getEntry(post);

        return posts;

    public Post findById(Long id) {
        Post post = postDAO.findById(id);

        org.hibernate.engine.spi.PersistenceContext persistenceContext = getHibernatePersistenceContext();

        EntityEntry entityEntry = persistenceContext.getEntry(post);

        return post;

    private org.hibernate.engine.spi.PersistenceContext getHibernatePersistenceContext() {
        SharedSessionContractImplementor session = entityManager.unwrap(
        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:

public void test() {
    Post newPost = forumService.newPost(
        "High-Performance Java Persistence", 

    List<Post> posts = forumService.findAllByTitle(
        "High-Performance Java Persistence"
    assertEquals(1, posts.size());

    Post post = forumService.findById(newPost.getId());
        "High-Performance Java Persistence", 

Cool, right?

Online Workshops

If you enjoyed this article, I bet you are going to love my upcoming Online Workshops!


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.

Transactions and Concurrency Control eBook

12 Comments on “Spring read-only transaction Hibernate optimization

  1. I use a Spring version less than 5.1 with Oracle DB. Do I need to use an annotation @Transactional readonly?

    • Yes, you can still benefit from setting the read-only attribute as it will disable the Persistence Context auto flushing mechanism.

  2. I faced off a strange behaviour (or would it be a normal behaviour?).
    I have a service A with @Transaction (read-write) that calls others services B, C, D. These others services are @Transaction(readOnly=true).
    What I noticed is that always the service read-only Transaction is called, one connection is borrowed from the pool.
    Therefore there is a connection for a read-write transaction and one call for connection from the pool for each service with read-only transaction.
    I don’t know if that behaviour is a configuration from Spring or Hibernate, but in a big scale that seems a real performance problem. I’d expect one borrow for read-write transaction and other (only) for read-only transaction.


    • That’s not the typical behavior. By default, the transaction propagates since the TransactionInterceptor uses a thread-local context. In your case, you need to debug it and see why it happens like that.

  3. Hi,
    In the spring boot world, the open session in view filter set flush mode to NEVER. As you said: “No flush, no dirty checking.”

    In this case, is there any hidden advantage by using @Transactional(readOnly=true) in methods that only load entities? Isn’t better to leave it without annotation?

    Snippet: https://gitlab.com/snippets/1913330

    Thank you.

  4. Hi Vlad, from the article:
    “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.”
    I can’t get it, how does FlushType.MANUAL disable dirty checking mechanism?

    • It’s simple. No flush, no dirty checking. MANUAL does not call flush automatically. So, unless you manually flush, no change will be propagated.

  5. Does it make sence not to start a transaction(TransactionAttributeType.NOT_SUPPORTED) at all for read-only find cases?

    • I wouldn’t do that. Suspending/resuming transactions adds an unnecessary overhead in this case.

    • Not a good solution, actually!
      Even, if you don’t open a transaction, the DB server automatically opens and closes a dedicated transaction for every SQL statement. That’s why, you’ll not benefit from disabling a transaction for a specific method!

      But, what you could get, is to run into problems, if your method will have more, than one SQL statement. In this case, every statement will be executed in a separate transaction with all the related issues.

      And, the last one, if your method would be called within another one with @Transactional annotation, according to the spec, the propagation NOT_SUPPORTED causes suspension of the transaction, started by the calling method. That is, such a method executes with no transaction at all, and then the transaction is resumed and another methods continue to operate in the initial transaction.

      • Very good comment. Also, NOT_SUPPORTED works with JTA out-of-the-box, as RESOURCE_LOCAL JDBC Drivers don’t support transaction resume/suspend properly. All in all, you are better of using REQUIRED when the service method needs database access, or avoid the @Transactional annotation when the service never calls a DAO or Repository, like when it simply calls an external REST service.

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.

Hypersistence Optimizer 2.2 has been released!