Configuring Ehcache 3 with Hibernate 6

Learn to setup and configure the L2 (second-level cache) in Hibernate 6 using Ehcache 3. This tutorial aims to provide an initial working setup and expects you to further study and customize the configuration according to your requirements.

1. Dependencies

This demo uses the hibernate’s built-in integration for JCache so we need to include the hibernate-jcache module.

<dependency>
      <groupId>org.hibernate.orm</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
</dependency>
<dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>javax.persistence-api</artifactId>
      <version>${javax.persistence.version}</version>
      <scope>provided</scope>
</dependency>
<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-jcache</artifactId>
      <version>${hibernate.version}</version>
</dependency>
<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-ehcache</artifactId>
      <version>${hibernate.ehcache.version}</version>
</dependency>

In addition, a JCache implementation needs to be added. We are using ehcache so its related modules need to be included in the dependencies.

<dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>ehcache</artifactId>
      <version>${ehcache.version}</version>
</dependency>

Since Java 11, JAXB has been removed from the JDK distribution so we need to import it explicitly. It is required to parse the ehcache.xml file when we bootstrap the application and the configuration is parsed.

<dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-core</artifactId>
      <version>${jaxb.core.version}</version>
</dependency>
<dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>${jaxb.api.version}</version>
</dependency>
<dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>${jaxb.api.version}</version>
</dependency>

Apart from the above-listed dependencies, we obviously need to add other required modules such as hibernate, persistence, datasource, logging and unit testing.

2. Enable L2 Cache Configuration

To enable the second-level cache support, we need to enable it in hibernate.cf.xml file or supplying properties in Java-based configuration for SessionFactory.

<property name="hibernate.cache.region.factory_class">jcache</property>
<property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property>
<property name="hibernate.javax.cache.uri">ehcache.xml</property>
<property name="hibernate.cache.use_second_level_cache">true</property>

The hibernate.cache.region.factory_class is used to declare the provider to use. Here we are using EhcacheCachingProvider that configures the ehcache for us.

We may enable the statistics to verify that cache is working as expected.

<property name="hibernate.generate_statistics">true</property>

Finally, define the entities specific cache settings in ehcache.xml.

<config
        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xmlns='http://www.ehcache.org/v3'
        xsi:schemaLocation="
        http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">

    <cache alias="employee">
        <key-type>java.lang.Long</key-type>
        <value-type>com.howtodoinjava.basics.entity.EmployeeEntity</value-type>
        <heap unit="entries">10000</heap>
    </cache>

</config>

3. Make Entities @Cacheable

The @Cacheable annotation is used to specify whether an entity should be stored in the second-level cache. And the @Cache annotation is used to specify the CacheConcurrencyStrategy of a root entity or a collection.

import jakarta.persistence.*;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Cache;
import java.io.Serial;
import java.io.Serializable;

@Entity
@Table(name = "Employee", uniqueConstraints = {
    @UniqueConstraint(columnNames = "ID"),
    @UniqueConstraint(columnNames = "EMAIL") })
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class EmployeeEntity implements Serializable {

      @Serial
      private static final long serialVersionUID = -1798070786993154676L;

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "ID", unique = true, nullable = false)
      private Integer employeeId;

      @Column(name = "EMAIL", unique = true, nullable = false, length = 100)
      private String email;

      @Column(name = "FIRST_NAME", nullable = false, length = 100)
      private String firstName;

      @Column(name = "LAST_NAME", nullable = false, length = 100)
      private String lastName;

      //Getters and setters are hidden for brevity
}

4. Demo

Now it’s time to test the second-level cache configuration by running a few tests. We are using JUnit 5 for executing tests that persist the data in H2 database.

To test the configuration, we have two options:

  • Verify the cache HIT statistics in the console
  • Use the sessionFactory.getStatistics().getSecondLevelCacheHitCount() method and ensure that it matches as expected.

In the given tests, we are creating an EmployeeEntity instance and saving it to the database. Then we fetch it from the database multiple times.

import com.howtodoinjava.basics.entity.EmployeeEntity;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Environment;
import org.junit.jupiter.api.*;

public class TestSecondLevelCache {

  private static SessionFactory sessionFactory = null;
  private Session session = null;

  @BeforeAll
  static void setup(){
    try {
      StandardServiceRegistry standardRegistry
          = new StandardServiceRegistryBuilder()
          .configure("hibernate-test.cfg.xml")
          .build();

      Metadata metadata = new MetadataSources(standardRegistry)
          .addAnnotatedClass(EmployeeEntity.class)
          .getMetadataBuilder()
          .build();

      sessionFactory = metadata
          .getSessionFactoryBuilder().build();

    } catch (Throwable ex) {
      throw new ExceptionInInitializerError(ex);
    }
  }

  @BeforeEach
  void setupThis(){
    session = sessionFactory.openSession();
    session.beginTransaction();
  }

  @AfterEach
  void tearThis(){
    session.getTransaction().commit();
  }

  @AfterAll
  static void tear(){
    sessionFactory.close();
  }

  @Test
  void createSessionFactoryWithXML() {
    EmployeeEntity emp = new EmployeeEntity();
    emp.setEmail("demo-user@mail.com");
    emp.setFirstName("demo");
    emp.setLastName("user");

    Assertions.assertNull(emp.getEmployeeId());

    session.persist(emp);

    Assertions.assertNotNull(emp.getEmployeeId());
    EmployeeEntity cachedEmployee = session.get(EmployeeEntity.class,
        emp.getEmployeeId());

    session.flush();
    session.close();

    //New Session

    session = sessionFactory.openSession();
    session.beginTransaction();


    cachedEmployee = session.get(EmployeeEntity.class,
        emp.getEmployeeId());

    Assertions.assertEquals(0,
        session.getSessionFactory().getStatistics().getSecondLevelCacheHitCount());

    session.flush();
    session.close();

    //New Session

    session = sessionFactory.openSession();
    session.beginTransaction();


    cachedEmployee = session.get(EmployeeEntity.class,
        emp.getEmployeeId());

    Assertions.assertEquals(1,
        session.getSessionFactory().getStatistics().getSecondLevelCacheHitCount());

    session.flush();
    session.close();

    //New Session

    session = sessionFactory.openSession();
    session.beginTransaction();


    cachedEmployee = session.get(EmployeeEntity.class,
        emp.getEmployeeId());

    Assertions.assertEquals(2,
        session.getSessionFactory().getStatistics().getSecondLevelCacheHitCount());
  }
}

The first time, the HIT count will be 0 and the PUT count will be 1.

27391000 nanoseconds spent performing 1 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
1195400 nanoseconds spent performing 1 L2C misses;

The second time, HIT count will be 1 and others will be 0.

0 nanoseconds spent performing 0 L2C puts;
1012300 nanoseconds spent performing 1 L2C hits;
0 nanoseconds spent performing 0 L2C misses;

Similarly, the HIT count will increment by 1 everytime we fetch the same entity over and over again. It proves that the L2 cache has been configured and working as expected.

5. Conclusion

In this hibernate tutorial, we learned to configure the Ehcache 3 with Hibernate 6. We used the hibernate’s internal JCache implementation and plugged in the Ehcache as cache provider.

Finally, we verified that caching is working as expected in a unit test.

Happy Learning !!

Sourcecode on Github

Was this post helpful?

Join 7000+ Awesome Developers

Get the latest updates from industry, awesome resources, blog updates and much more.

* We do not spam !!

Leave a Comment

HowToDoInJava

A blog about Java and related technologies, the best practices, algorithms, and interview questions.