JPA Level 1 Cache and Hibernate

Introduction

In this article, I am going to explain how the JPA and Hibernate first layer mechanism works and how it can improve the performance of your data access layer.

In JPA terminology, the first level cache is called the Persistence Context and is represented by the EntityManager interface. In Hibernate, the first level cache is represented by the Session interface, which is an extension to the JPA EntityManager.

JPA Entity States and Associated Transition Methods

A JPA entity can be in one of the following states:

To change the state of an entity, you can use the persist, merge or remove JPA EntityManager methods as shown in the following diagram:

When you call the persist method, the state of the entity changes from New to Managed.

And when the find method is called, the entity state becomes Managed as well.

After closing the EntityManager or calling the evict method, the entity state becomes Detached.

When an entity is passed to the remove JPA method of the EntityManager, the state of the entity becomes Removed.

Implementing the first level cache in Hibernate

Internally, Hibernate stores entities in the following map:

Map<EntityUniqueKey, Object> entitiesByUniqueKey = new HashMap<>(INIT_COLL_SIZE);

A EntityUniqueKey is defined as follows:

public class EntityUniqueKey implements Serializable {
 
    private final String entityName;
     
    private final String uniqueKeyName;
     
    private final Object key;
     
    private final Type keyType;
     
    ...
 
    @Override
    public boolean equals(Object other) {
        EntityUniqueKey that = (EntityUniqueKey) other;
         
        return that != null &&
            that.entityName.equals(entityName) &&
            that.uniqueKeyName.equals(uniqueKeyName) &&
            keyType.isEqual(that.key, key);
    }
     
    ...
}

When the state of the entity becomes Managed, this means it is stored in this Java Map entitiesByUniqueKey

So, in JPA and Hibernate, the first level cache is the Java Map, where the Map key is represented by an object encapsulating the entity name and its identifier, and the Map value is the entity object itself.

Therefore, in JPA EntityManager or Hibernate Session can only be one entity, stored using the same id and type of entity class.

The reason we can have at most one entity view stored in the first level cache is because otherwise we can get different mappings of the same database row without knowing which one is the correct version. synchronized with the corresponding database record.

Transactional lazy write

To understand the benefits of using L1 cache, you need to understand how it works. transactional lazy write strategy

As already explained, the methods persist, merge and remove JPA EntityManager change the state of the given entity. However, the state of the entity is not synchronized every time the method is called EntityManager… In fact, state changes are only synchronized when the method is executed flush EntityManager.

This cache synchronization strategy is called write-behind and looks like this:

The advantage of using a write-behind strategy is that we can batch process multiple objects when clearing the first level cache.

The write-behind strategy is actually very common. The processor also has caches of the first, second and third levels. And when the registry changes, its state is out of sync with main memory until a flush is performed.

Also, as explained in this article, the relational database system maps OS pages to in-memory buffer pool pages, and for performance reasons, the buffer pool is synchronized periodically during a checkpoint rather than on every transaction commit.

Recurring reads at the application level

When you get a JPA entity, either directly:

Post post = entityManager.find(Post.class, 1L);

Or through a request:

Post post = entityManager.createQuery("""
    select p
    from Post p
    where p.id = :id
    """, Post.class)
.setParameter("id", 1L)
.getSingleResult();

The Hibernate event will be triggered LoadEntityEvent… Event LoadEntityEvent processed DefaultLoadEventListenerwhich will load the entity like this:

Hibernate first checks to see if the entity is stored in the first level cache, and if so, it returns the current managed reference to it.

In case the JPA entity is not found in the first level cache, Hibernate checks the second level cache if this cache is enabled.

If the entity is not found in the first or second level cache, then Hibernate will load it from the database using an SQL query.

First level cache guarantees repeatability of reading entities at the application levelbecause no matter how many times the entity is loaded from the Persistence Context, the same managed entity reference will be returned to the caller.

When the entity is loaded from the database, Hibernate takes the JDBC ResultSet and converts it to Java Object[]which is known as the state of the loaded entity. The loaded state is stored in the first level cache along with the managed entity, as shown in the following diagram:

As you can see from the above diagram, the second-level cache stores the loaded state, so when loading an entity that was previously stored in the second-level cache, we can get the loaded state without having to execute the corresponding SQL query.

For this reason, loading an entity takes up more memory than the Java entity object itself, since the loaded state must also be stored. When you flush the JPA Persistence Context, the loaded state will be used by dirty checking to determine if the entity has changed since it was first loaded. If it has changed, a SQL UPDATE command will be generated.

Therefore, if you do not plan to change the entity, then it is more efficient to load it into read-only modebecause the loaded state will be discarded after the entity object is instantiated.

Conclusion

Level 1 cache is a required construct in JPA and Hibernate. Since it is tied to the currently running thread, it cannot be shared across multiple users. For this reason, in JPA and Hibernate, the first level cache is not thread safe.

In addition to providing repetitive reads at the application level, the first-level cache can batch process multiple SQL statements at the time of flushing, thereby improving read-write transaction response times.

However, while it prevents multiple calls to get the same entity from the database find, but it cannot prevent JPQL or SQL from loading the last snapshot of the entity from the database, only to discard it when assembling the query result set.


Material prepared as part of the course “Java Developer. Professional “.

We invite everyone to a free demo lesson “Using kafka to communicate microservices in Java Spring Boot.” We continue to develop a system for obtaining the exchange rate. Let’s get acquainted with kafka and use it to organize the interconnection of a pair of microservices.
>> REGISTRATION

Similar Posts

Leave a Reply