UPDATE: The below approaches have not aged well. See Hibernate with Infinispan for a more up-to-date L2 cache solution.

Introduction

I needed to set up Hibernate cacheing and wanted to also use an L2 cache (Ehcache, in this case). Having not done this for quite a long time, I looked at a tutorial which led me to a simple set-up which worked well. However, that set-up used a relatively old version of Ehcache.

That initial set-up is shown below, followed by a more modern approach.

Set-up without JCache

I initially used the following dependencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.30.Final</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

<!-- extra for 2nd level cache -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.4.30.Final</version>
</dependency>

Here is the hibernate.cfg.xml file - placed in src/main/resources for this standalone Java SE application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.url">jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</property>
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>
        <property name="show_sql">true</property>
        <property name="hbm2ddl.auto">create</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
        <property name="hibernate.generate_statistics">false</property>

        <!-- extra for 2nd level cache -->
        <!-- https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#caching-config -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

       <!-- mappings must be placed after properties -->
        <mapping class="org.ajames.hibernateorm.Person"/>
    </session-factory>

</hibernate-configuration>

The Person class I used for basic testing of my set-up:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Cacheable;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;

@Entity
//annotations for 2nd-level cache:
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Person implements Serializable {

    @Id
    @GeneratedValue(generator = "sequence-generator")
    @GenericGenerator(
            name = "sequence-generator",
            strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
            parameters = {
                @Parameter(name = "sequence_name", value = "user_sequence"),
                @Parameter(name = "initial_value", value = "1"),
                @Parameter(name = "increment_size", value = "1")
            }
    )
    private int id;

    @Column(name = "FULL_NAME")
    private String name;

    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getters and setters not shown

    @Override
    public String toString() {
        return String.join(", ", Integer.toString(id), name, Integer.toString(age));
    }

}

Here is the main application class I used to create Hibernate and the L1 and L2 caches. It is not intended to be production-ready code - it is just a demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
//import org.hibernate.cfg.Configuration;
//import org.hibernate.service.ServiceRegistry;

public class App {

    public static void main(String[] args) {
        App app = new App();

        // legacy approach:
        //try ( SessionFactory sessionFactory = new Configuration().configure()
        //        .buildSessionFactory()) {
        //    app.persist(sessionFactory);
        //    app.loadAll(sessionFactory);
        //}

        // recommended approach:
        try ( SessionFactory sessionFactory = app.createSessionFactory()) {
            app.persist(sessionFactory);
            app.loadAll(sessionFactory);
        }
    }

    private SessionFactory createSessionFactory() throws Exception {
        final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
                .configure() // from hibernate.cfg.xml
                .build();

        SessionFactory sessionFactory = null;
        try {
            sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory();
        } catch (Exception e) {
            StandardServiceRegistryBuilder.destroy(registry);
        }
        return sessionFactory;
    }

    private void persist(SessionFactory sessionFactory) {
        Person p1 = new Person("John", 35);
        Person p2 = new Person("Tina", 30);
        System.out.println();
        System.out.println("-- persisting persons (before) --");
        System.out.printf(" -> %s%n -> %s%n", p1, p2);

        Session session = sessionFactory.openSession();
        session.beginTransaction();
        session.save(p1);
        session.save(p2);
        session.getTransaction().commit();

        System.out.println();
        System.out.println("-- persisting persons (after) --");
        System.out.printf(" -> %s%n -> %s%n", p1, p2);
    }

    private void loadAll(SessionFactory sessionFactory) {
        System.out.println();
        System.out.println("-- loading persons --");
        try ( Session session = sessionFactory.openSession()) {
            List<Person> persons = session.createQuery("FROM Person p", Person.class).list();
            persons.forEach((x) -> System.out.printf(" -> %s%n", x));
        }
    }

}

Deprecation warning

The above libraries generate a runtime warning message:

WARN: HHH020100: The Ehcache second-level cache provider for Hibernate is deprecated. See https://hibernate.atlassian.net/browse/HHH-12441 for details.

The link leads to a ticket which states:

Deprecate hibernate-ehcache module as it is using Ehcache 2 as its back-end, which is deprecated itself in favor of Ehcache 3. Ehcache 3 can be easily used instead by using the hibernate-jcache module and have Ehcache 3 (which is a JCache implementor) properly registered with JCache.

Specifically, in my case, the above set-up uses Ehcache 2.10.6, whereas the current release of Ehcache is 3.9.2.

Side note: The “official” current release of Ehcache is shown as version 3.8.1 on their official web site, and is currently tagged as the “latest release” in GitHub. This was released in September 2019. However, Maven offers subsequent releases, including v3.9.2 from February 2021 - so that is what I will use here.

Using JCache

JCache is an API - so, it does not provide a caching implementation. But it does allow implementation providers to deliver products which conform to the API - as is the case with Ehcache 3.

This is exposed through the javax.cache package (in cache-api-1.0.0.jar).

Dependencies

The new dependencies are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.30.Final</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

<!-- extra for 2nd level cache -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jcache</artifactId>
    <version>5.4.30.Final</version>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.2</version>
</dependency>

Configuration

The new hibernate.cfg.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.url">jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</property>
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>
        <property name="show_sql">true</property>
        <property name="hbm2ddl.auto">create</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
        <property name="hibernate.generate_statistics">false</property>

        <!-- extra for 2nd level cache -->
        <!-- https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#caching-config -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <property name="hibernate.cache.region.factory_class">jcache</property>
        <property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property>
        <!-- placed on the classpath (note: "classpath:ehcache.xml" does not work! -->
        <property name="hibernate.javax.cache.uri">ehcache.xml</property>
        <!--
        <property name="hibernate.cache.use_query_cache">true</property>
        -->

       <!-- mappings must be placed after properties -->
        <mapping class="org.ajames.hibernateorm.Person"/>
    </session-factory>

</hibernate-configuration>

The ehcache.xml - using a very basic cache template for finer-grained control over the Ehcache cache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <service>
        <jsr107:defaults enable-management="true" enable-statistics="true"/>
    </service>

    <cache-template name="simple">
        <expiry>
            <ttl unit="minutes">2</ttl>
        </expiry>
        <heap unit="entries">100</heap>

    </cache-template>

    <cache alias="org.ajames.hibernateormnative.Person" uses-template="simple"/>

</config>

The Java code is unchanged - so, we are still using classes such as org.hibernate.SessionFactory. In this sense, we have not taken full advantage of JCache, as we still have explicit dependencies on Ehcache classes. And we’re using ehcache.xml

The Java code could be re-written to use only the JCache API. Maybe another time. I am not sure about the ehcache.xml config file. That would need a closer look.

More interestingly, I have specified this in ehcache.xml:

enable-statistics="true"

To get statistics on the L2 cache, I will use the JMX API - see the next post for details: Getting L2 Cache Statistics using Mbeans.