My old approach to using Hibernate with a L2 cache has not aged well.

Here is a newer version. It uses Infinispan instead of Ehcache - because that integration is provided for us, and the Hibernate-Ehcache integration does not seem to be maintained. This also does not use JCache (although I suppose it could be enhanced for that).

In this very simple demo, I use an in-memory H2 database.

There is no custom Infinispan config file, here.

The Maven dependencies:

 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
<dependencies>

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.1.7.Final</version>
    </dependency>

    <dependency>
        <groupId>org.infinispan</groupId>
        <artifactId>infinispan-hibernate-cache-v60</artifactId>
        <version>14.0.7.Final</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.7</version>
    </dependency>

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

</dependencies>

My 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
33
34
35
36
37
38
39
40
41
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

    <session-factory>

        <!-- Database connection settings -->
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.url">jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1</property>
        <property name="connection.username">sa</property>
        <property name="connection.password"></property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">1</property>

        <!-- SQL dialect -->
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>

        <!-- Infinispan L2 cache -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <property name="hibernate.cache.region.factory_class">infinispan</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
        <property name="hibernate.show_sql">true</property>

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

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create</property>

        <!-- The annotated entity class -->
        <mapping class="com.northcoder.hibernate6.Event"/>

    </session-factory>

</hibernate-configuration>

My entity class (based on the tutorial bundled with the Hibernate 6 release zip - so, yes, it uses Date instead of java.time).

See here for different cache concurrency strategies.

 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
package com.northcoder.hibernate6;

import jakarta.persistence.Cacheable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.util.Date;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "EVENTS")
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Event {

    private Long id;

    private String title;
    private Date date;

    public Event() {
        // this form used by Hibernate
    }

    public Event(String title, Date date) {
        // for application use, to create new events
        this.title = title;
        this.date = date;
    }

    @Id
    @GeneratedValue(generator = "increment")
    @GenericGenerator(name = "increment", strategy = "increment")
    public Long getId() {
        return id;
    }

    private void setId(Long id) {
        this.id = id;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "EVENT_DATE")
    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

The demo class:

 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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.northcoder.hibernate6;

import java.util.Date;
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.stat.CacheRegionStatistics;
import org.hibernate.stat.Statistics;

public class App {

    private SessionFactory sessionFactory;

    public static void main(String[] args) {
        App app = new App();
        app.setup();
        app.createEvents();
        app.fetchEvent();
        app.shutDown();
    }

    private void setup() {
        final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
                .configure() // configures settings from hibernate.cfg.xml
                .build();
        try {
            sessionFactory = new MetadataSources(registry)
                    .buildMetadata()
                    .buildSessionFactory();
        } catch (Exception e) {
            StandardServiceRegistryBuilder.destroy(registry);
        }
    }

    private void createEvents() {
        // create a couple of events...
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        session.persist(new Event("Our very first event!", new Date()));
        session.persist(new Event("A follow up event", new Date()));
        session.getTransaction().commit();
        session.close();

        session = sessionFactory.openSession();
        session.beginTransaction();
        List<Event> result = session.createQuery("from Event", Event.class).list();
        System.out.println();
        for (Event event : result) {
            System.out.println("Event (" + event.getDate() + ") : " + event.getTitle());
        }
        System.out.println();
        session.getTransaction().commit();

        Event event = session.get(Event.class, 1L);
        System.out.println("Event (" + event.getDate() + ") : " + event.getTitle());
        System.out.println();

        session.close();
    }

    private void fetchEvent() {
        try (Session session = sessionFactory.openSession()) {
            System.out.println();
            Event event = session.get(Event.class, 1L);
            System.out.println("Event from fetch (" + event.getDate() + ") : " + event.getTitle());
            System.out.println();

            // example of getting statistics programmatically
            Statistics statistics = session.getSessionFactory().getStatistics();
            CacheRegionStatistics secondLevelCacheStatistics
                    = statistics.getDomainDataRegionStatistics("com.northcoder.hibernate6.Event");
            long hitCount = secondLevelCacheStatistics.getHitCount();
            long missCount = secondLevelCacheStatistics.getMissCount();
            double hitRatio = (double) hitCount / (hitCount + missCount);
            System.out.println("Fetch hit count: " + hitCount);
            System.out.println("Fetch miss count: " + missCount);
            System.out.println("Fetch hit ratio: " + hitRatio);
            System.out.println();
        }
    }

    private void shutDown() {
        if (sessionFactory != null) {
            sessionFactory.close();
        }
    }
}

The above code includes an example of reading cache statistics programmatically - so, for example:

1
2
3
4
5
Event from fetch (2023-03-27 15:18:23.866) : Our very first event!

Fetch hit count: 1
Fetch miss count: 0
Fetch hit ratio: 1.0

But it also generates the following information, based on the Hibernate configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
INFO: Session Metrics {
    0 nanoseconds spent acquiring 0 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    0 nanoseconds spent preparing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    336800 nanoseconds spent performing 1 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

These are logged whenever a session closes - which may result in a lot of unwanted logging info. You can suppress these session metrics by adding the following to hibernate.cfg.xml

1
<property name="hibernate.session.events.log">false</property>