Ads Top

Hibernate Second Level Cache - Spring Boot + JPA + EhCache


Introduction

In this blog post, we will implement the Hibernate Second-Level cache to boost application performance using Spring Boot, JPA, and EhCache.

Table of Contents

  1. What is Hibernate Second-Level Cache
  2. How Second-Level Cache Works
  3. Cache Concurrency Strategy
  4. Second-Level Cache Example 

What is Hibernate Second-Level Cache

Hibernate Second-Level cache is created in the session factory scope and is available globally to the whole Application. Unlike the First Level cache which is enabled by default, Second-Level cache is not enabled by default and to use it, we need to use a third-party cache provider. Some of the third-party implementations available are EhCache, OS Cache, Swarm Cache, JBoss Cache, etc. In this post, we will be using EhCache implementation to demonstrate Hibernate Second-Level cache using Spring Boot and JPA.

How Second-Level Cache Works

Whenever we try to load an entity, Hibernate session checks if the cached copy of the entity is present in First Level Cache. If the cached copy of the entity is present in First Level Cache then it is returned from there.

If the cached copy of the entity is not found in the First-Level cache, then Hibernate session search for the cached copy of the entity in Second-Level cache. If the cached copy of the entity is present in Second-Level cache, then it is returned from Second-Level cache and it is also saved in the First-Level cache so that the next time entity can be returned from the First-Level cache itself.

If the cached copy of the entity is not present in both First-Level cache and Second-Level cache, then the entity is fetched from the database and the entity is saved in both First-Level cache and Second-Level cache.

When any modification is made to the entity using Hibernate session APIs, then the Hibernate Second-Level cache validates itself for the updated entities. If the modification is done directly in the database, then there is no way Hibernate-Second level cache validates itself until "timeToLiveSeconds" for that cache region has passed.

Cache Concurrency Strategy

When we enable Second-Level cache, we have to decide which concurrency strategy to use for our persistence class.

Read-only: Use this strategy when you are sure that your data never changes. If you try to update the data with this strategy Hibernate will throw an exception.

Read-write: Use this strategy when you do lots of updates on your entity. It guarantees data consistency when multiple transactions try to access the same object. The transaction which accesses the object first acquires the lock and other transactions will not have access to the cache and will start fetching the data directly from the database.

Nonstrict-read-write:  It is similar to Read-write but there is no locking mechanism hence it does not guarantee data consistency between cache and database. Use this strategy when stale data for a small window is not a concern.

Transactional: It is suitable in a JTA environment. Any changes in the cached entity will be committed or rollback in the same transaction.

Second-Level Cache Example

Let's set up a Spring Boot application using JPA. We will enable Second-Level cache and use EhCache as a cache provider. For this demo app, we will use the H2 database.

Now go to https://start.spring.io/ and add below 3 dependencies to generate a Spring Boot App:
  • Spring Web
  • Spring Data JPA
  • H2 Database

Now extract the generated zip file and open the Spring Boot App in your favorite IDE. Add the below dependency in your pom.xml file.
 <dependency>  
      <groupId>org.hibernate</groupId>  
      <artifactId>hibernate-ehcache</artifactId>  
      <version>5.4.12.Final</version>  
 </dependency>  

Please note that I have used the version 5.4.12.Final for the above EhCache dependency. This version should match with the Hibernate Version you are using. You can find out the Hibernate version you are using in the transitive dependencies of Spring Data JPA in your IDE.

For example, in IntelliJ click on the Maven tab on the right-hand side and then click on the Dependencies to see the dependency tree.


Your final pom.xml should be like this:
 <?xml version="1.0" encoding="UTF-8"?>  
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  
      <modelVersion>4.0.0</modelVersion>  
      <parent>  
           <groupId>org.springframework.boot</groupId>  
           <artifactId>spring-boot-starter-parent</artifactId>  
           <version>2.2.5.RELEASE</version>  
           <relativePath/> <!-- lookup parent from repository -->  
      </parent>  
      <groupId>com.devtalkers</groupId>  
      <artifactId>hibernate2levelcache</artifactId>  
      <version>0.0.1-SNAPSHOT</version>  
      <name>hibernate2levelcache</name>  
      <description>Demo project for Spring Boot</description>  
      <properties>  
           <java.version>1.8</java.version>  
      </properties>  
      <dependencies>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-data-jpa</artifactId>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-web</artifactId>  
           </dependency>  
           <dependency>  
                <groupId>org.hibernate</groupId>  
                <artifactId>hibernate-ehcache</artifactId>  
                <version>5.4.12.Final</version>  
           </dependency>  
           <dependency>  
                <groupId>com.h2database</groupId>  
                <artifactId>h2</artifactId>  
                <scope>runtime</scope>  
           </dependency>  
           <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-test</artifactId>  
                <scope>test</scope>  
                <exclusions>  
                     <exclusion>  
                          <groupId>org.junit.vintage</groupId>  
                          <artifactId>junit-vintage-engine</artifactId>  
                     </exclusion>  
                </exclusions>  
           </dependency>  
      </dependencies>  
      <build>  
           <plugins>  
                <plugin>  
                     <groupId>org.springframework.boot</groupId>  
                     <artifactId>spring-boot-maven-plugin</artifactId>  
                </plugin>  
           </plugins>  
      </build>  
 </project>  


Now open the application.properties file and paste the below configuration to configure EhCache:

application.properties

1:  spring.jpa.properties.hibernate.cache.use_second_level_cache=true  
2:  spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory  
3:  spring.jpa.properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE  
4:    
5:  spring.datasource.url=jdbc:h2:mem:testdb  
6:  spring.datasource.driverClassName=org.h2.Driver  
7:  spring.datasource.username=sa  
8:  spring.datasource.password=  
9:    
10:  spring.jpa.database-plateform=org.hibernate.dialect.H2Dialect  
11:  spring.jpa.hibernate.ddl-auto=update  
12:    
13:  spring.h2.console.enabled=true  
14:    
15:  logging.level.org.hibernate.SQL=DEBUG  
16:  logging.level.org.hibernate.type=TRACE  

Below is the line by line description on what we have configured in the above application.properties.

  • At line no. 1, we have enabled the second-level cache.  
  • Then at line no. 2, we have specified the cache provider as EhCache.
  • Line no. 3, we have set the cache mode to ENABLE_SELECTIVE, which means the cache is enabled for all entities for which cache annotation is specified.  
  • From line no. 5 to 13, we have set up our H2 database
  • At last, from line no. 15 to 16, we have configured the hibernate logs to print hibernate queries on the console so that we can verify that after caching our calls are not going to database. 

Now we will create an entity class called City inside the newly created package called entity. We will apply the Hibernate-Second level cache on this entity class.

City.java 

1:  package com.devtalkers.hibernate2levelcache.entity;  
2:    
3:  import org.hibernate.annotations.CacheConcurrencyStrategy;  
4:    
5:  import javax.persistence.*;  
6:    
7:  @Entity(name = "city")  
8:  @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  
9:  public class City {  
10:    
11:    @Id  
12:    @Column(name = "id")  
13:    @GeneratedValue(strategy = GenerationType.IDENTITY)  
14:    private Integer id;  
15:    
16:    @Column(name = "name")  
17:    private String name;  
18:    
19:    @Column(name = "population")  
20:    private Long population;  
21:    
22:    public Integer getId() {  
23:      return id;  
24:    }  
25:    
26:    public void setId(Integer id) {  
27:      this.id = id;  
28:    }  
29:    
30:    public String getName() {  
31:      return name;  
32:    }  
33:    
34:    public void setName(String name) {  
35:      this.name = name;  
36:    }  
37:    
38:    public Long getPopulation() {  
39:      return population;  
40:    }  
41:    
42:    public void setPopulation(Long population) {  
43:      this.population = population;  
44:    }  
45:    
46:    @Override  
47:    public String toString() {  
48:      return "City{" +  
49:          "id=" + id +  
50:          ", name='" + name + '\'' +  
51:          ", population=" + population +  
52:          '}';  
53:    }  
54:  }  
55:    

The above City class contains three attributes id, name, and population. Also, note that at line no. 2, we have used the cache annotation with concurrency strategy READ_WRITE. 

Next, create a new package repository and create a CityRepository interface which will extend JpaRepository.

 package com.devtalkers.hibernate2levelcache.repository;  
   
 import com.devtalkers.hibernate2levelcache.entity.City;  
 import org.springframework.data.jpa.repository.JpaRepository;  
 import org.springframework.stereotype.Repository;  
   
 @Repository  
 public interface CityRepository extends JpaRepository<City, Integer> {  
 }  
   

Now, we will create the class CityService, which will act as a service layer inside the package service. Here we will Autowired the CityRepository and add two methods getCityById and saveCity.

 package com.devtalkers.hibernate2levelcache.service;  
   
 import com.devtalkers.hibernate2levelcache.entity.City;  
 import com.devtalkers.hibernate2levelcache.repository.CityRepository;  
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.stereotype.Service;  
   
 @Service  
 public class CityService {  
   
   @Autowired  
   private CityRepository cityRepository;  
   
   public City getCityById(Integer id){  
     return cityRepository.findById(id).get();  
   }  
   
   public City saveCity(City city){  
     return cityRepository.save(city);  
   }  
   
 }  
   

getCityById method simply returns the city by city ID. saveCity method is used to save the city in the database. To call these methods, we will create a Rest Controller named CityController in which we will create below two rest endpoints:

GET: http://localhost:8080/cities/1 - Fetch the city by ID
POST: http://localhost:8080/cities - Save the city

Below is our Rest Controller class CityController:
1:  package com.devtalkers.hibernate2levelcache.rest;  
2:    
3:  import com.devtalkers.hibernate2levelcache.entity.City;  
4:  import com.devtalkers.hibernate2levelcache.service.CityService;  
5:  import org.springframework.beans.factory.annotation.Autowired;  
6:  import org.springframework.http.HttpStatus;  
7:  import org.springframework.http.ResponseEntity;  
8:  import org.springframework.web.bind.annotation.*;  
9:    
10:  @RestController  
11:  public class CityController {  
12:    
13:    @Autowired  
14:    private CityService cityService;  
15:    
16:    @PostMapping("/cities")  
17:    public ResponseEntity<City> saveCity(@RequestBody City city){  
18:      return new ResponseEntity<>(cityService.saveCity(city), HttpStatus.CREATED);  
19:    }  
20:    
21:    @GetMapping("/cities/{id}")  
22:    public ResponseEntity<City> getCityById(@PathVariable(name = "id") Integer id){  
23:      return new ResponseEntity<>(cityService.getCityById(id), HttpStatus.OK);  
24:    }  
25:  }  
26:    

The saveCity and getCityById method at lines 17 and 22 in the above CityController class simply delegate the calls to service class called CityService.
Now the coding part is finished, let's verify that the Hibernate Second-Level caching is working or not. First, start the Spring Boot app and call the below API to save the city. We will use the Postman to run our APIs.

POST: http://localhost:8080/cities
{
    "name": "Pune",
    "population": 89845
}


As you can see from the above screenshot, a city has been created with an ID of 1.

Now, call the GET API and retrieve the city by ID of 1. The first call to get the city by ID 1 will be fetched from the database and then it will be saved in the cache.

GET: http://localhost:8080/cities/1

Since we have enabled the Hibernate SQL logs in the application.properties file, we can observe that the data is fetched from the database for the first attempt to fetch the city details by ID.
 2020-04-02 16:23:07.839 DEBUG 22972 --- [nio-8080-exec-3] org.hibernate.SQL            : select city0_.id as id1_0_0_, city0_.name as name2_0_0_, city0_.population as populati3_0_0_ from city city0_ where city0_.id=?  
 2020-04-02 16:23:07.841 TRACE 22972 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder   : binding parameter [1] as [INTEGER] - [1]  

Now, if you try to hit the City GET API again, you will observe from the logs that there is no database query being executed, which means the data is fetched from the cache.

Conclusion

In this post, we have learned what is Second-Level cache, different cache concurrency strategy and, how to implement Second-Level cache using Spring Boot, JPA, and EhCache.

3 comments:

  1. well explained article, Thanks a lot. I have gone through many article but couldn't find worthy. You have explained things neatly. Thank you. Could you pls write an article explaining query caching.

    ReplyDelete
  2. I am facing the below issue could you please help
    Caused by: java.lang.IllegalArgumentException: No enum constant javax.persistence.SharedCacheMode.ENABLE_SELECTIVE

    ReplyDelete
  3. i am facing below issue ,please help me on this issue
    Caused by: java.lang.IllegalArgumentException: No enum constant javax.persistence.SharedCacheMode.ENABLE_SELECTIVE

    ReplyDelete

Powered by Blogger.