|
| 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. |
0 commit comments