diff --git a/README.adoc b/README.adoc index a5364a18..6d150d30 100644 --- a/README.adoc +++ b/README.adoc @@ -51,6 +51,7 @@ All tutorials are documented in AsciiDoc format and published as an https://anto |link:data-jpa-audit[Spring Data JPA Audit] |Enable Audit with Spring Data JPA |link:data-jpa-event[Spring Data JPA: Event Driven] |Implement `Entity` validation at `Repository` level through `EventListeners` |link:data-jpa-filtered-query[Spring Data JPA: Global Filtered Query] |Implement global filtered query with Spring Data JPA by defining `repositoryBaseClass` +|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 |link:data-mongodb-audit[Spring Data MongoDB Audit] |Enable Audit with Spring Data MongoDB |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 |link:data-mongodb-transactional[Spring Data MongoDB: Transactional] |Enable `@Transactional` support for Spring Data MongoDB diff --git a/data-jpa-hibernate-cache/.gitattributes b/data-jpa-hibernate-cache/.gitattributes new file mode 100644 index 00000000..8af972cd --- /dev/null +++ b/data-jpa-hibernate-cache/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/data-jpa-hibernate-cache/.gitignore b/data-jpa-hibernate-cache/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/data-jpa-hibernate-cache/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/data-jpa-hibernate-cache/README.adoc b/data-jpa-hibernate-cache/README.adoc new file mode 100644 index 00000000..be033e41 --- /dev/null +++ b/data-jpa-hibernate-cache/README.adoc @@ -0,0 +1,201 @@ += Spring Data JPA: Hibernate Second Level Caching with EhCache +:source-highlighter: highlight.js +Rashidi Zin +1.0, July 19, 2025 +:toc: +:nofooter: +:icons: font +:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-jpa-hibernate-cache + +Implement Hibernate second level caching using Spring Data JPA and EhCache to improve application performance. + +== Background + +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. + +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. + +In this tutorial, we will implement Hibernate second-level cache using Spring Data JPA and EhCache as the caching provider. + +== Implementation + +=== Dependencies + +First, we need to add the necessary dependencies to our project. For a Gradle project using Kotlin DSL: + +[source,kotlin] +---- +dependencies { + implementation("org.ehcache:ehcache::jakarta") + implementation("org.hibernate.orm:hibernate-jcache") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + // Other dependencies... +} +---- + +The key dependencies are: + +* `org.ehcache:ehcache::jakarta` - EhCache implementation with Jakarta EE support +* `org.hibernate.orm:hibernate-jcache` - Hibernate JCache integration, which allows Hibernate to use JCache-compatible caching providers like EhCache + +=== Configuration + +Next, we need to configure Hibernate to use EhCache as the second-level cache provider. This is done in the `application.properties` file: + +[source,properties] +---- +spring.jpa.properties.hibernate.cache.region.factory_class=jcache +spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml +spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider +spring.jpa.properties.hibernate.cache.use_second_level_cache=true +---- + +These properties configure Hibernate to: + +* Use JCache as the caching implementation (`hibernate.cache.region.factory_class=jcache`) +* Use the EhCache configuration file located at `/ehcache.xml` (`hibernate.javax.cache.uri=/ehcache.xml`) +* Use EhCache as the JCache provider (`hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider`) +* Enable the second-level cache (`hibernate.cache.use_second_level_cache=true`) + +=== EhCache Configuration + +We need to create an EhCache configuration file (`ehcache.xml`) in the resources directory: + +[source,xml] +---- + + + + 10 + + + +---- + +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. + +=== Entity Configuration + +Finally, we need to configure our entities to use the second-level cache. This is done using the `@Cache` annotation from Hibernate: + +[source,java] +---- +@Entity +@Cache(usage = READ_WRITE, region = "customer") +class Customer { + + @Id + @GeneratedValue + private Long id; + + private String name; + +} +---- + +The `@Cache` annotation has two important attributes: + +* `usage` - Specifies the cache concurrency strategy. In this case, we're using `READ_WRITE`, which is appropriate for entities that can be updated. +* `region` - Specifies the cache region (or name) to use. This should match the cache alias defined in the EhCache configuration file. + +== Validation + +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: + +[source,java] +---- +@DataJpaTest(properties = { + "spring.jpa.hibernate.ddl-auto=create-drop", + "spring.jpa.properties.hibernate.generate_statistics=true" +}) +@Import(TestcontainersConfiguration.class) +@Sql(statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')", executionPhase = BEFORE_TEST_CLASS) +@TestMethodOrder(OrderAnnotation.class) +class CustomerRepositoryTests { + + @Autowired + private CustomerRepository customers; + + private Statistics statistics; + + @BeforeEach + void setupStatistics(@Autowired EntityManagerFactory entityManagerFactory) { + statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics(); + } + + @Test + @Order(1) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On initial retrieval data will be retrieved from the database and customer cache will be stored") + void initial() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCachePutCount()).isEqualTo(1); + assertThat(statistics.getSecondLevelCacheHitCount()).isZero(); + } + + @Test + @Order(2) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On subsequent retrieval data will be retrieved from the customer cache") + void subsequent() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCacheHitCount()).isEqualTo(1); + } + +} +---- + +This test does the following: + +1. Enables Hibernate statistics with `spring.jpa.properties.hibernate.generate_statistics=true` +2. Uses Testcontainers to set up a PostgreSQL database for testing +3. Inserts a test customer record before the test class runs +4. Orders the tests to ensure they run in sequence +5. Gets the Hibernate Statistics object from the EntityManagerFactory +6. In the first test (`initial`), it verifies that on the initial retrieval: + * The data is fetched from the database and stored in the cache (cache put count = 1) + * The data is not fetched from the cache (cache hit count = 0) +7. In the second test (`subsequent`), it verifies that on subsequent retrieval: + * The data is fetched from the cache (cache hit count = 1) + +The test configuration uses a simple Testcontainers setup: + +[source,java] +---- +@TestConfiguration(proxyBeanMethods = false) +public class TestcontainersConfiguration { + + @Bean + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); + } + +} +---- + +== Benefits of Second-Level Caching + +Implementing Hibernate second-level caching with EhCache offers several benefits: + +1. **Improved Performance**: By caching frequently accessed entities, we reduce the number of database queries, resulting in faster response times. +2. **Reduced Database Load**: Fewer database queries mean less load on the database server, which can improve overall system performance. +3. **Scalability**: With proper caching, applications can handle more concurrent users without proportionally increasing database load. +4. **Flexibility**: EhCache offers various configuration options, such as cache size, expiration policies, and storage options (heap, off-heap, disk). + +== Considerations + +While second-level caching can significantly improve performance, there are some considerations to keep in mind: + +1. **Cache Invalidation**: When data is updated in the database by external processes, the cache may become stale. Consider implementing cache invalidation strategies. +2. **Memory Usage**: Caching consumes memory, so it's important to monitor memory usage and adjust cache sizes accordingly. +3. **Concurrency**: In a multi-node environment, consider using a distributed cache to ensure cache consistency across nodes. +4. **Selective Caching**: Not all entities benefit from caching. Focus on caching frequently accessed, rarely changed entities. + +== Conclusion + +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. + +By implementing second-level caching, we can improve the performance of our Spring Data JPA applications, reduce database load, and enhance scalability. \ No newline at end of file diff --git a/data-jpa-hibernate-cache/build.gradle.kts b/data-jpa-hibernate-cache/build.gradle.kts new file mode 100644 index 00000000..66491d77 --- /dev/null +++ b/data-jpa-hibernate-cache/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + java + id("org.springframework.boot") version "3.5.3" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "zin.rashidi" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.ehcache:ehcache::jakarta") + implementation("org.hibernate.orm:hibernate-jcache") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + runtimeOnly("org.postgresql:postgresql") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:postgresql") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/data-jpa-hibernate-cache/settings.gradle.kts b/data-jpa-hibernate-cache/settings.gradle.kts new file mode 100644 index 00000000..bd5512b8 --- /dev/null +++ b/data-jpa-hibernate-cache/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "data-jpa-hibernate-cache" diff --git a/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/DataJpaHibernateCacheApplication.java b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/DataJpaHibernateCacheApplication.java new file mode 100644 index 00000000..6ab09f15 --- /dev/null +++ b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/DataJpaHibernateCacheApplication.java @@ -0,0 +1,13 @@ +package zin.rashidi.datajpa.hibernatecache; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DataJpaHibernateCacheApplication { + + public static void main(String[] args) { + SpringApplication.run(DataJpaHibernateCacheApplication.class, args); + } + +} diff --git a/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/Customer.java b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/Customer.java new file mode 100644 index 00000000..a3a2990e --- /dev/null +++ b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/Customer.java @@ -0,0 +1,23 @@ +package zin.rashidi.datajpa.hibernatecache.customer; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.Cache; + +import static org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE; + +/** + * @author Rashidi Zin + */ +@Entity +@Cache(usage = READ_WRITE, region = "customer") +class Customer { + + @Id + @GeneratedValue + private Long id; + + private String name; + +} diff --git a/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepository.java b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepository.java new file mode 100644 index 00000000..11458979 --- /dev/null +++ b/data-jpa-hibernate-cache/src/main/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepository.java @@ -0,0 +1,9 @@ +package zin.rashidi.datajpa.hibernatecache.customer; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Rashidi Zin + */ +interface CustomerRepository extends JpaRepository { +} diff --git a/data-jpa-hibernate-cache/src/main/resources/application.properties b/data-jpa-hibernate-cache/src/main/resources/application.properties new file mode 100644 index 00000000..e46532d6 --- /dev/null +++ b/data-jpa-hibernate-cache/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=data-jpa-hibernate-cache +spring.jpa.properties.hibernate.cache.region.factory_class=jcache +spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml +spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider +spring.jpa.properties.hibernate.cache.use_second_level_cache=true \ No newline at end of file diff --git a/data-jpa-hibernate-cache/src/main/resources/ehcache.xml b/data-jpa-hibernate-cache/src/main/resources/ehcache.xml new file mode 100644 index 00000000..dbcba613 --- /dev/null +++ b/data-jpa-hibernate-cache/src/main/resources/ehcache.xml @@ -0,0 +1,7 @@ + + + + 10 + + + \ No newline at end of file diff --git a/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestDataJpaHibernateCacheApplication.java b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestDataJpaHibernateCacheApplication.java new file mode 100644 index 00000000..ffcd8550 --- /dev/null +++ b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestDataJpaHibernateCacheApplication.java @@ -0,0 +1,11 @@ +package zin.rashidi.datajpa.hibernatecache; + +import org.springframework.boot.SpringApplication; + +public class TestDataJpaHibernateCacheApplication { + + public static void main(String[] args) { + SpringApplication.from(DataJpaHibernateCacheApplication::main).with(TestcontainersConfiguration.class).run(args); + } + +} diff --git a/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestcontainersConfiguration.java b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestcontainersConfiguration.java new file mode 100644 index 00000000..bedc6fa6 --- /dev/null +++ b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/TestcontainersConfiguration.java @@ -0,0 +1,18 @@ +package zin.rashidi.datajpa.hibernatecache; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +@TestConfiguration(proxyBeanMethods = false) +public class TestcontainersConfiguration { + + @Bean + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); + } + +} diff --git a/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepositoryTests.java b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepositoryTests.java new file mode 100644 index 00000000..82090a63 --- /dev/null +++ b/data-jpa-hibernate-cache/src/test/java/zin/rashidi/datajpa/hibernatecache/customer/CustomerRepositoryTests.java @@ -0,0 +1,62 @@ +package zin.rashidi.datajpa.hibernatecache.customer; + +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; +import zin.rashidi.datajpa.hibernatecache.TestcontainersConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + +/** + * @author Rashidi Zin + */ +@DataJpaTest(properties = { + "spring.jpa.hibernate.ddl-auto=create-drop", + "spring.jpa.properties.hibernate.generate_statistics=true" +}) +@Import(TestcontainersConfiguration.class) +@Sql(statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')", executionPhase = BEFORE_TEST_CLASS) +@TestMethodOrder(OrderAnnotation.class) +class CustomerRepositoryTests { + + @Autowired + private CustomerRepository customers; + + private Statistics statistics; + + @BeforeEach + void setupStatistics(@Autowired EntityManagerFactory entityManagerFactory) { + statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics(); + } + + @Test + @Order(1) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On initial retrieval data will be retrieved from the database and customer cache will be stored") + void initial() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCachePutCount()).isEqualTo(1); + assertThat(statistics.getSecondLevelCacheHitCount()).isZero(); + } + + @Test + @Order(2) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On subsequent retrieval data will be retrieved from the customer cache") + void subsequent() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCacheHitCount()).isEqualTo(1); + } + +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 97fc7019..8a6db27e 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -12,6 +12,7 @@ ** xref:data-jpa-audit.adoc[JPA Audit] ** xref:data-jpa-event.adoc[Event Driven] ** xref:data-jpa-filtered-query.adoc[Global Filtered Query] +** xref:data-jpa-hibernate-cache.adoc[Implement Hibernate Second Level Caching] ** xref:data-mongodb-audit.adoc[MongoDB Audit] ** xref:data-mongodb-full-text-search.adoc[MongoDB Full Text Search] ** xref:data-mongodb-transactional.adoc[MongoDB Transactional] diff --git a/docs/modules/ROOT/pages/data-jpa-hibernate-cache.adoc b/docs/modules/ROOT/pages/data-jpa-hibernate-cache.adoc new file mode 100644 index 00000000..54eb15bf --- /dev/null +++ b/docs/modules/ROOT/pages/data-jpa-hibernate-cache.adoc @@ -0,0 +1,199 @@ += Spring Data JPA: Hibernate Second Level Caching with EhCache +:source-highlighter: highlight.js +Rashidi Zin +1.0, July 19, 2025 +:icons: font +:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-jpa-hibernate-cache + +Implement Hibernate second level caching using Spring Data JPA and EhCache to improve application performance. + +== Background + +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. + +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. + +In this tutorial, we will implement Hibernate second-level cache using Spring Data JPA and EhCache as the caching provider. + +== Implementation + +=== Dependencies + +First, we need to add the necessary dependencies to our project. For a Gradle project using Kotlin DSL: + +[source,kotlin] +---- +dependencies { + implementation("org.ehcache:ehcache::jakarta") + implementation("org.hibernate.orm:hibernate-jcache") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + // Other dependencies... +} +---- + +The key dependencies are: + +* `org.ehcache:ehcache::jakarta` - EhCache implementation with Jakarta EE support +* `org.hibernate.orm:hibernate-jcache` - Hibernate JCache integration, which allows Hibernate to use JCache-compatible caching providers like EhCache + +=== Configuration + +Next, we need to configure Hibernate to use EhCache as the second-level cache provider. This is done in the `application.properties` file: + +[source,properties] +---- +spring.jpa.properties.hibernate.cache.region.factory_class=jcache +spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml +spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider +spring.jpa.properties.hibernate.cache.use_second_level_cache=true +---- + +These properties configure Hibernate to: + +* Use JCache as the caching implementation (`hibernate.cache.region.factory_class=jcache`) +* Use the EhCache configuration file located at `/ehcache.xml` (`hibernate.javax.cache.uri=/ehcache.xml`) +* Use EhCache as the JCache provider (`hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider`) +* Enable the second-level cache (`hibernate.cache.use_second_level_cache=true`) + +=== EhCache Configuration + +We need to create an EhCache configuration file (`ehcache.xml`) in the resources directory: + +[source,xml] +---- + + + + 10 + + + +---- + +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. + +=== Entity Configuration + +Finally, we need to configure our entities to use the second-level cache. This is done using the `@Cache` annotation from Hibernate: + +[source,java] +---- +@Entity +@Cache(usage = READ_WRITE, region = "customer") +class Customer { + + @Id + @GeneratedValue + private Long id; + + private String name; + +} +---- + +The `@Cache` annotation has two important attributes: + +* `usage` - Specifies the cache concurrency strategy. In this case, we're using `READ_WRITE`, which is appropriate for entities that can be updated. +* `region` - Specifies the cache region (or name) to use. This should match the cache alias defined in the EhCache configuration file. + +== Validation + +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: + +[source,java] +---- +@DataJpaTest(properties = { + "spring.jpa.hibernate.ddl-auto=create-drop", + "spring.jpa.properties.hibernate.generate_statistics=true" +}) +@Import(TestcontainersConfiguration.class) +@Sql(statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')", executionPhase = BEFORE_TEST_CLASS) +@TestMethodOrder(OrderAnnotation.class) +class CustomerRepositoryTests { + + @Autowired + private CustomerRepository customers; + + private Statistics statistics; + + @BeforeEach + void setupStatistics(@Autowired EntityManagerFactory entityManagerFactory) { + statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics(); + } + + @Test + @Order(1) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On initial retrieval data will be retrieved from the database and customer cache will be stored") + void initial() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCachePutCount()).isEqualTo(1); + assertThat(statistics.getSecondLevelCacheHitCount()).isZero(); + } + + @Test + @Order(2) + @Transactional(propagation = REQUIRES_NEW) + @DisplayName("On subsequent retrieval data will be retrieved from the customer cache") + void subsequent() { + customers.findById(1L).orElseThrow(); + + assertThat(statistics.getSecondLevelCacheHitCount()).isEqualTo(1); + } + +} +---- + +This test does the following: + +1. Enables Hibernate statistics with `spring.jpa.properties.hibernate.generate_statistics=true` +2. Uses Testcontainers to set up a PostgreSQL database for testing +3. Inserts a test customer record before the test class runs +4. Orders the tests to ensure they run in sequence +5. Gets the Hibernate Statistics object from the EntityManagerFactory +6. In the first test (`initial`), it verifies that on the initial retrieval: +* The data is fetched from the database and stored in the cache (cache put count = 1) +* The data is not fetched from the cache (cache hit count = 0) +7. In the second test (`subsequent`), it verifies that on subsequent retrieval: +* The data is fetched from the cache (cache hit count = 1) + +The test configuration uses a simple Testcontainers setup: + +[source,java] +---- +@TestConfiguration(proxyBeanMethods = false) +public class TestcontainersConfiguration { + + @Bean + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); + } + +} +---- + +== Benefits of Second-Level Caching + +Implementing Hibernate second-level caching with EhCache offers several benefits: + +1. **Improved Performance**: By caching frequently accessed entities, we reduce the number of database queries, resulting in faster response times. +2. **Reduced Database Load**: Fewer database queries mean less load on the database server, which can improve overall system performance. +3. **Scalability**: With proper caching, applications can handle more concurrent users without proportionally increasing database load. +4. **Flexibility**: EhCache offers various configuration options, such as cache size, expiration policies, and storage options (heap, off-heap, disk). + +== Considerations + +While second-level caching can significantly improve performance, there are some considerations to keep in mind: + +1. **Cache Invalidation**: When data is updated in the database by external processes, the cache may become stale. Consider implementing cache invalidation strategies. +2. **Memory Usage**: Caching consumes memory, so it's important to monitor memory usage and adjust cache sizes accordingly. +3. **Concurrency**: In a multi-node environment, consider using a distributed cache to ensure cache consistency across nodes. +4. **Selective Caching**: Not all entities benefit from caching. Focus on caching frequently accessed, rarely changed entities. + +== Conclusion + +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. + +By implementing second-level caching, we can improve the performance of our Spring Data JPA applications, reduce database load, and enhance scalability. \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 03e57166..dc2dd8ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ include("data-jdbc-schema-generation") include("data-jpa-audit") include("data-jpa-event") include("data-jpa-filtered-query") +include("data-jpa-hibernate-cache") include("data-mongodb-audit") include("data-mongodb-full-text-search") include("data-mongodb-tc-data-load")