Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Second-level cache

The persistence context is a first-level cache, but it lives only as long as one EntityManager — usually one request. The second-level cache is shared by the whole EntityManagerFactory: an entity loaded in one request can be taken from memory in the next one without a database query. It suits data that is read often and changed rarely, such as tags or categories.

Dependencies

Hibernate stores the cache through the JCache standard; the storage itself is provided by a library, here Caffeine:

<!-- pom.xml -->
<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jcache</artifactId>
  <version>7.4.12.Final</version>
</dependency>
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>jcache</artifactId>
  <version>3.3.0</version>
</dependency>

Settings

<!-- src/main/resources/META-INF/persistence.xml -->
<persistence-unit name="blog">
  <class>com.example.blog.Author</class>
  <class>com.example.blog.Post</class>
  <class>com.example.blog.Comment</class>
  <class>com.example.blog.Tag</class>
  <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
  <properties>
    <!-- connection properties as before -->
    <property name="hibernate.cache.region.factory_class" value="jcache"/>
    <property name="hibernate.javax.cache.missing_cache_strategy" value="create"/>
    <property name="hibernate.generate_statistics" value="true"/>
  </properties>
</persistence-unit>

ENABLE_SELECTIVE means "cache only the entities marked for it". missing_cache_strategy lets Hibernate create the cache regions itself, without a separate Caffeine configuration file.

Marking an entity

// src/main/java/com/example/blog/Tag.java
@Entity
@Cacheable
public class Tag {
    // fields as before
}

@Cacheable comes from jakarta.persistence.

Checking it

// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> em.persist(new Tag("java")));
 
for (int i = 1; i <= 3; i++) {
    Tag tag = emf.callInTransaction(em -> em.find(Tag.class, 1L));
    IO.println("Request " + i + ": " + tag.getName());
}
 
Statistics stats = emf.unwrap(SessionFactory.class).getStatistics();
IO.println("Cache hits: " + stats.getSecondLevelCacheHitCount()
        + ", misses: " + stats.getSecondLevelCacheMissCount());

Every callInTransaction uses a new EntityManager, so the first-level cache does not help here. Output:

INFO: HHH90001028: Second-level cache region factory [org.hibernate.cache.jcache.internal.JCacheRegionFactory]
Hibernate: insert into Tag (name,id) values (?,?)
Request 1: java
Request 2: java
Request 3: java
Cache hits: 3, misses: 0

Not a single select: the tag went into the cache when it was saved, and all three requests took it from there. Without @Cacheable the same program runs select t1_0.id,t1_0.name from Tag t1_0 where t1_0.id=? three times and reports Cache hits: 0, misses: 0.

What to keep in mind

  • The cache knows only about changes made through Hibernate. If another application or a native SQL update changes the table, the cache returns stale data.
  • In a cluster of several servers each one has its own Caffeine cache; for shared caching you need a distributed provider.
  • Start without the cache, measure, and add it only to the entities where it really saves queries.

Common mistake

The entity cache works for find and for loading relationships, but not for queries. A JPQL query by name still goes to the database every time:

// src/main/java/com/example/blog/Main.java
Tag tag = emf.callInTransaction(em -> em
        .createQuery("select t from Tag t where t.name = :name", Tag.class)
        .setParameter("name", "java")
        .getSingleResult());
Hibernate: select t1_0.id,t1_0.name from Tag t1_0 where t1_0.name=?
Request 1: java
Hibernate: select t1_0.id,t1_0.name from Tag t1_0 where t1_0.name=?
Request 2: java

Hibernate has a separate query cache for this, but it is rarely worth it: it is invalidated on every change of the table.

Thanks for reading!

Donate to the site — we work hard for you.