Skip to content

Commit 51e3c1c

Browse files
authored
Add example for Hibernate second level caching with EhCache (#254)
1 parent 9cbc8b7 commit 51e3c1c

File tree

17 files changed

+626
-0
lines changed

17 files changed

+626
-0
lines changed

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ All tutorials are documented in AsciiDoc format and published as an https://anto
5151
|link:data-jpa-audit[Spring Data JPA Audit] |Enable Audit with Spring Data JPA
5252
|link:data-jpa-event[Spring Data JPA: Event Driven] |Implement `Entity` validation at `Repository` level through `EventListeners`
5353
|link:data-jpa-filtered-query[Spring Data JPA: Global Filtered Query] |Implement global filtered query with Spring Data JPA by defining `repositoryBaseClass`
54+
|link:data-jpa-hibernate-cache[Spring Data JPA: Hibernate Second Level Caching with EhCache] |Implement Hibernate second level caching using Spring Data JPA and EhCache to improve application performance
5455
|link:data-mongodb-audit[Spring Data MongoDB Audit] |Enable Audit with Spring Data MongoDB
5556
|link:data-mongodb-full-text-search[Spring Data MongoDB: Full Text Search] |Implement link:https://docs.mongodb.com/manual/text-search/[MongoDB Full Text Search] with Spring Data MongoDB
5657
|link:data-mongodb-transactional[Spring Data MongoDB: Transactional] |Enable `@Transactional` support for Spring Data MongoDB
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/gradlew text eol=lf
2+
*.bat text eol=crlf
3+
*.jar binary

data-jpa-hibernate-cache/.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/

data-jpa-hibernate-cache/README.adoc

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
= Spring Data JPA: Hibernate Second Level Caching with EhCache
2+
:source-highlighter: highlight.js
3+
Rashidi Zin <rashidi@zin.my>
4+
1.0, July 19, 2025
5+
:toc:
6+
:nofooter:
7+
:icons: font
8+
:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-jpa-hibernate-cache
9+
10+
Implement Hibernate second level caching using Spring Data JPA and EhCache to improve application performance.
11+
12+
== Background
13+
14+
In a typical Spring Data JPA application, when an entity is retrieved from the database, it is stored in the first-level cache (Persistence Context). However, this cache is short-lived and tied to a specific transaction or EntityManager. Once the transaction is completed, the cached entities are no longer available.
15+
16+
This is where Hibernate's second-level cache comes into play. The second-level cache is a session-factory-level cache that is shared across all sessions created by the same session factory. This means that entities can be cached and reused across multiple transactions, reducing database load and improving application performance.
17+
18+
In this tutorial, we will implement Hibernate second-level cache using Spring Data JPA and EhCache as the caching provider.
19+
20+
== Implementation
21+
22+
=== Dependencies
23+
24+
First, we need to add the necessary dependencies to our project. For a Gradle project using Kotlin DSL:
25+
26+
[source,kotlin]
27+
----
28+
dependencies {
29+
implementation("org.ehcache:ehcache::jakarta")
30+
implementation("org.hibernate.orm:hibernate-jcache")
31+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
32+
// Other dependencies...
33+
}
34+
----
35+
36+
The key dependencies are:
37+
38+
* `org.ehcache:ehcache::jakarta` - EhCache implementation with Jakarta EE support
39+
* `org.hibernate.orm:hibernate-jcache` - Hibernate JCache integration, which allows Hibernate to use JCache-compatible caching providers like EhCache
40+
41+
=== Configuration
42+
43+
Next, we need to configure Hibernate to use EhCache as the second-level cache provider. This is done in the `application.properties` file:
44+
45+
[source,properties]
46+
----
47+
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
48+
spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml
49+
spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
50+
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
51+
----
52+
53+
These properties configure Hibernate to:
54+
55+
* Use JCache as the caching implementation (`hibernate.cache.region.factory_class=jcache`)
56+
* Use the EhCache configuration file located at `/ehcache.xml` (`hibernate.javax.cache.uri=/ehcache.xml`)
57+
* Use EhCache as the JCache provider (`hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider`)
58+
* Enable the second-level cache (`hibernate.cache.use_second_level_cache=true`)
59+
60+
=== EhCache Configuration
61+
62+
We need to create an EhCache configuration file (`ehcache.xml`) in the resources directory:
63+
64+
[source,xml]
65+
----
66+
<config xmlns='http://www.ehcache.org/v3'>
67+
<cache alias="customer">
68+
<resources>
69+
<offheap unit="MB">10</offheap>
70+
</resources>
71+
</cache>
72+
</config>
73+
----
74+
75+
This configuration defines a cache named "customer" with 10MB of off-heap memory. Off-heap memory is memory that is allocated outside the Java heap, which can help reduce garbage collection pressure.
76+
77+
=== Entity Configuration
78+
79+
Finally, we need to configure our entities to use the second-level cache. This is done using the `@Cache` annotation from Hibernate:
80+
81+
[source,java]
82+
----
83+
@Entity
84+
@Cache(usage = READ_WRITE, region = "customer")
85+
class Customer {
86+
87+
@Id
88+
@GeneratedValue
89+
private Long id;
90+
91+
private String name;
92+
93+
}
94+
----
95+
96+
The `@Cache` annotation has two important attributes:
97+
98+
* `usage` - Specifies the cache concurrency strategy. In this case, we're using `READ_WRITE`, which is appropriate for entities that can be updated.
99+
* `region` - Specifies the cache region (or name) to use. This should match the cache alias defined in the EhCache configuration file.
100+
101+
== Validation
102+
103+
To validate that our second-level cache is working correctly, we can use Hibernate's statistics API to check cache hits and misses. Here's a test that demonstrates this:
104+
105+
[source,java]
106+
----
107+
@DataJpaTest(properties = {
108+
"spring.jpa.hibernate.ddl-auto=create-drop",
109+
"spring.jpa.properties.hibernate.generate_statistics=true"
110+
})
111+
@Import(TestcontainersConfiguration.class)
112+
@Sql(statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')", executionPhase = BEFORE_TEST_CLASS)
113+
@TestMethodOrder(OrderAnnotation.class)
114+
class CustomerRepositoryTests {
115+
116+
@Autowired
117+
private CustomerRepository customers;
118+
119+
private Statistics statistics;
120+
121+
@BeforeEach
122+
void setupStatistics(@Autowired EntityManagerFactory entityManagerFactory) {
123+
statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
124+
}
125+
126+
@Test
127+
@Order(1)
128+
@Transactional(propagation = REQUIRES_NEW)
129+
@DisplayName("On initial retrieval data will be retrieved from the database and customer cache will be stored")
130+
void initial() {
131+
customers.findById(1L).orElseThrow();
132+
133+
assertThat(statistics.getSecondLevelCachePutCount()).isEqualTo(1);
134+
assertThat(statistics.getSecondLevelCacheHitCount()).isZero();
135+
}
136+
137+
@Test
138+
@Order(2)
139+
@Transactional(propagation = REQUIRES_NEW)
140+
@DisplayName("On subsequent retrieval data will be retrieved from the customer cache")
141+
void subsequent() {
142+
customers.findById(1L).orElseThrow();
143+
144+
assertThat(statistics.getSecondLevelCacheHitCount()).isEqualTo(1);
145+
}
146+
147+
}
148+
----
149+
150+
This test does the following:
151+
152+
1. Enables Hibernate statistics with `spring.jpa.properties.hibernate.generate_statistics=true`
153+
2. Uses Testcontainers to set up a PostgreSQL database for testing
154+
3. Inserts a test customer record before the test class runs
155+
4. Orders the tests to ensure they run in sequence
156+
5. Gets the Hibernate Statistics object from the EntityManagerFactory
157+
6. In the first test (`initial`), it verifies that on the initial retrieval:
158+
* The data is fetched from the database and stored in the cache (cache put count = 1)
159+
* The data is not fetched from the cache (cache hit count = 0)
160+
7. In the second test (`subsequent`), it verifies that on subsequent retrieval:
161+
* The data is fetched from the cache (cache hit count = 1)
162+
163+
The test configuration uses a simple Testcontainers setup:
164+
165+
[source,java]
166+
----
167+
@TestConfiguration(proxyBeanMethods = false)
168+
public class TestcontainersConfiguration {
169+
170+
@Bean
171+
@ServiceConnection
172+
PostgreSQLContainer<?> postgresContainer() {
173+
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
174+
}
175+
176+
}
177+
----
178+
179+
== Benefits of Second-Level Caching
180+
181+
Implementing Hibernate second-level caching with EhCache offers several benefits:
182+
183+
1. **Improved Performance**: By caching frequently accessed entities, we reduce the number of database queries, resulting in faster response times.
184+
2. **Reduced Database Load**: Fewer database queries mean less load on the database server, which can improve overall system performance.
185+
3. **Scalability**: With proper caching, applications can handle more concurrent users without proportionally increasing database load.
186+
4. **Flexibility**: EhCache offers various configuration options, such as cache size, expiration policies, and storage options (heap, off-heap, disk).
187+
188+
== Considerations
189+
190+
While second-level caching can significantly improve performance, there are some considerations to keep in mind:
191+
192+
1. **Cache Invalidation**: When data is updated in the database by external processes, the cache may become stale. Consider implementing cache invalidation strategies.
193+
2. **Memory Usage**: Caching consumes memory, so it's important to monitor memory usage and adjust cache sizes accordingly.
194+
3. **Concurrency**: In a multi-node environment, consider using a distributed cache to ensure cache consistency across nodes.
195+
4. **Selective Caching**: Not all entities benefit from caching. Focus on caching frequently accessed, rarely changed entities.
196+
197+
== Conclusion
198+
199+
In this tutorial, we've implemented Hibernate second-level caching using Spring Data JPA and EhCache. We've configured the necessary dependencies, set up the cache configuration, and annotated our entities to use the cache. We've also demonstrated how to validate that the cache is working correctly using Hibernate's statistics API.
200+
201+
By implementing second-level caching, we can improve the performance of our Spring Data JPA applications, reduce database load, and enhance scalability.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
plugins {
2+
java
3+
id("org.springframework.boot") version "3.5.3"
4+
id("io.spring.dependency-management") version "1.1.7"
5+
}
6+
7+
group = "zin.rashidi"
8+
version = "0.0.1-SNAPSHOT"
9+
10+
java {
11+
toolchain {
12+
languageVersion = JavaLanguageVersion.of(21)
13+
}
14+
}
15+
16+
repositories {
17+
mavenCentral()
18+
}
19+
20+
dependencies {
21+
implementation("org.ehcache:ehcache::jakarta")
22+
implementation("org.hibernate.orm:hibernate-jcache")
23+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
24+
runtimeOnly("org.postgresql:postgresql")
25+
testImplementation("org.springframework.boot:spring-boot-starter-test")
26+
testImplementation("org.springframework.boot:spring-boot-testcontainers")
27+
testImplementation("org.testcontainers:junit-jupiter")
28+
testImplementation("org.testcontainers:postgresql")
29+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
30+
}
31+
32+
tasks.withType<Test> {
33+
useJUnitPlatform()
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = "data-jpa-hibernate-cache"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zin.rashidi.datajpa.hibernatecache;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class DataJpaHibernateCacheApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(DataJpaHibernateCacheApplication.class, args);
11+
}
12+
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package zin.rashidi.datajpa.hibernatecache.customer;
2+
3+
import jakarta.persistence.Entity;
4+
import jakarta.persistence.GeneratedValue;
5+
import jakarta.persistence.Id;
6+
import org.hibernate.annotations.Cache;
7+
8+
import static org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE;
9+
10+
/**
11+
* @author Rashidi Zin
12+
*/
13+
@Entity
14+
@Cache(usage = READ_WRITE, region = "customer")
15+
class Customer {
16+
17+
@Id
18+
@GeneratedValue
19+
private Long id;
20+
21+
private String name;
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package zin.rashidi.datajpa.hibernatecache.customer;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
5+
/**
6+
* @author Rashidi Zin
7+
*/
8+
interface CustomerRepository extends JpaRepository<Customer, Long> {
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
spring.application.name=data-jpa-hibernate-cache
2+
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
3+
spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml
4+
spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
5+
spring.jpa.properties.hibernate.cache.use_second_level_cache=true

0 commit comments

Comments
 (0)