Redis Cache with Spring Data JPA

Redis Cache with Spring Data JPA | Redis is a open source software which can be used as In memory database and cache manager and message broker (MQ).

In Memory database:- It is like empty memory. We don’t have to write SQL queries, No tables, no sequence, no Joins. Data is stored as plain string format, List, Set or we can perform Hash operations. It includes in-built services for transcation mangement, clear memory, and e.t.c.

To work with redis we have to add the following dependency:- Spring Data Redis (Access + Driver).

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

In STS, Ecllipse, CTRL + Shift + T, type RedisConnectionFactory. Open RedisConnectionFactory interface and Function-key + F4 it will show implemntation classes.

RedisConnectionFactory has two implementation classes:-

  • JedisConnectionFactory
  • LettuceConnectionFactory

Among these Jedis is old driver, and Lettuce is new driver.

To create connection between application and redis database, use RedisConnectionFactory(I) which has 2 implemenation classes JedisConnectionFactory and LettuceConnectionFactory. Also provide properties for redis (host and port).

@Configuration
public class AppConfig {

    @Bean
    public RedisConnectionFactory cf() {
        return new LettuceConnectionFactory();
    }
}
# redis key-val (properties)
spring.redis.host=localhost
spring.redis.port=6379

Redis as Database Example

Let us see redis as database. While using redis as cache, internally redis will work as database to store the information.

Create one Spring Boot starter project and add the dependencies:- Lombok, Spring Data Redis (Access + Driver)

In application.properties:-

# redis key-val (properties)
spring.redis.host=localhost
spring.redis.port=6379

Model class (it must implement Serializable interface):-

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer stdId;
    private String stdName;
    private Double stdFee;
}

Spring configration file will contain redis connection and redis template. Configuration:-

@Configuration
public class AppConfig {

    // Redis connection
    @Bean
    public RedisConnectionFactory cf() {
        return new LettuceConnectionFactory();
    }

    // Redis template
    @Bean
    public RedisTemplate<String, Student> rt() {
        RedisTemplate<String, Student> template = new RedisTemplate<>();
        template.setConnectionFactory(cf());
        return template;
    }
}

Define one interface to perform the operation. StudentDaoImpl <– using HashOperations. Here:-

  • H (HRef) = String (‘STUDENT’)
  • HK (key) = Integer (stdId)
  • HV (val) = Student Type
public interface IStudentDao {
    void addStudent(Student s);

    void modifyStudent(Student s);

    Student getOneStudent(Integer id);

    Map<Integer, Student> getAllStudent();

    void removeStudent(Integer id);
}
@Repository
public class StudentDaoImpl implements IStudentDao {

    private final String KEY = "STUDENT";

    // ref type, keyType, valType
    @Resource(name = "rt")
    private HashOperations<String, Integer, Student> opr;

    @Override
    public void addStudent(Student s) {
        // create new record in HashMemory if given id not exist
        opr.putIfAbsent(KEY, s.getStdId(), s);
    }

    @Override
    public void modifyStudent(Student s) {
        // update data with given ID
        opr.put(KEY, s.getStdId(), s);
    }

    @Override
    public Student getOneStudent(Integer id) {
        // read one record based on HashRef and ID
        return opr.get(KEY, id);
    }

    @Override
    public Map<Integer, Student> getAllStudent() {
        // hashRef, get all rows as Map
        return opr.entries(KEY);
    }

    @Override
    public void removeStudent(Integer id) {
        // hashRef, key
        opr.delete(KEY, id);
    }
}

Runner class for test:-

@Component
public class RedisOprTest implements CommandLineRunner {

    @Autowired
    private IStudentDao dao;

    @Override
    public void run(String... args) throws Exception {
        dao.addStudent(new Student(101, "SAM", 500.25));
        dao.addStudent(new Student(102, "SYED", 800.25));
        dao.addStudent(new Student(103, "Rocco", 600.25));

        dao.getAllStudent().forEach((k, v) -> System.out.println(k + "-" + v));

        dao.removeStudent(101);
        dao.modifyStudent(new Student(103, "Jerry", 600.25));

        System.out.println("After remove & modify:-");
        dao.getAllStudent().forEach((k, v) -> System.out.println(k + "-" + v));
    }

}

Output after running the application:-

Redis as Cache Example

Cache is a temporary memory. It is used between Server (application) and database. This is used to reduce network calls if we trying to fetch same data multiple times from database.

Do not implment cache for all modules. Select modules which are actually mostly used. Example:- In gmail app multiple modules are there like inbox, sent, drafts, and e.t.c. Among them inbox is the mostly used.

Cache supports 3 operations:-

  • @Cacheable: Fetch data from database to application and store in cache.
  • @CachePut: Update data at cache while it is updating in database.
  • @CacheEvict: Remove data at cache while it is removing in database

Syntax:- @Cache___(value="CACHE-REGION", key="#Variable")

These operations are effected to selected region. Region is a area/memory part of the cache created for one module. Example:- EMP-REG, STD-REG, and e.t.c.

Unlike Hibernate, Spring Data JPA does not support SessionFactory and therefore 1st level cache (Session) and second level cache (Session Factory Cache) is not supported since Spring Boot 2.x. Spring Data JPA works on its own cache managers.

Let us first see application example without using cache

Example Without Cache

Add the following dependencies:-

  • Lombok
  • Spring Data JPA
  • MySQL Driver
  • Spring Web
  • Spring Boot DevTools
  • Spring Boot Actuator

In properties file:-

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
management.endpoints.web.exposure.include=*

In Model class:-

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    private Integer id;
    private String empName;
    private Double empSal;
}
public interface EmployeeRepository extends JpaRepository<Employee, Integer> { }
public interface IEmployeeService {
    public Employee saveEmployee(Employee e);

    public Employee updateEmployee(Integer empId, Employee e);

    public void deleteEmployee(Integer id);

    public List<Employee> getAllEmployees();

    public Employee getOneEmployee(Integer id);
}
@Service
public class EmployeeServiceImpl implements IEmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Override
    public Employee saveEmployee(Employee e) {
        return employeeRepository.save(e);
    }

    @Override
    public Employee updateEmployee(Integer empId, Employee e) {
        Employee employee = getOneEmployee(empId);
        employee.setEmpName(e.getEmpName());
        employee.setEmpSal(e.getEmpSal());
        return employeeRepository.save(e);
    }

    @Override
    public void deleteEmployee(Integer empId) {
        Employee employee = getOneEmployee(empId);
        employeeRepository.delete(employee);
    }

    @Override
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    public Employee getOneEmployee(Integer empId) {
        return employeeRepository.findById(empId)
                .orElseThrow(() -> new ResourceNotFoundException("Employee Not Found"));
    }

}
@ResponseStatus(value= HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    
    private static final long serialVersionUID = 1L;

    public ResourceNotFoundException(String message) {
        super(message);
    }
}
@RestController
@RequestMapping("/employee")
public class EmployeeRestContoller {

    @Autowired
    private IEmployeeService service;

    @PostMapping
    public ResponseEntity<Employee> saveEmployee(@RequestBody Employee e) {
        return new ResponseEntity<Employee>(service.saveEmployee(e), HttpStatus.CREATED);
    }

    @GetMapping
    public ResponseEntity<List<Employee>> getAllEmployees() {
        return ResponseEntity.ok(service.getAllEmployees());
    }

    @GetMapping("/{eid}")
    public ResponseEntity<Employee> getOneEmployee(@PathVariable Integer eid) {
        return new ResponseEntity<>(service.getOneEmployee(eid), HttpStatus.FOUND);
    }

    @PutMapping("/{eid}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Integer eid, 
                       @RequestBody Employee employee) {
        return new ResponseEntity<>(service.updateEmployee(eid, employee),
                       HttpStatus.ACCEPTED);
    }

    @DeleteMapping("/{eid}")
    public ResponseEntity<Void> deleteEmployee(@PathVariable Integer eid) {
        service.deleteEmployee(eid);
        return ResponseEntity.ok().build();
    }

}

Created some entries in the database. The findAll() gives (GET – http://localhost:8080/employee):-

[
    {
        "id": 1,
        "empName": "SAM",
        "empSal": 200.0
    },
    {
        "id": 2,
        "empName": "AJAY",
        "empSal": 400.0
    },
    {
        "id": 3,
        "empName": "SYED",
        "empSal": 600.0
    }
]

Whenever we try to fetch the particular employee (GET – {{url}}/employee/1) then each time application make a call to the database through below query:-

Hibernate: 
    select
        e1_0.id,
        e1_0.emp_name,
        e1_0.emp_sal 
    from
        employee e1_0 
    where
        e1_0.id=?

Introducing Redis Cache to the Application

Step-1:- Add the Spring Data Redis (Access + Driver) dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Step-2:- In starter class enable caching.

@SpringBootApplication
@EnableCaching
public class RedisAsCacheDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisAsCacheDemoApplication.class, args);
    }

}

Step-3: Add redis properties.

spring.cache.type=redis
spring.cache.redis.cache-null-values=true
# time-to-live is in milli second
# 60000 milli second = 1 minute
spring.cache.redis.time-to-live=60000

Step-4:- Add appriorate annotation (@Cacheable, @CachePut, @CacheEvict) to required methods.

@Override
@Cacheable(value="employees", key="#empId")
public Employee getOneEmployee(Integer empId) {
    return employeeRepository.findById(empId)
        .orElseThrow(() -> new ResourceNotFoundException("Employee Not Found"));
}

Here key and method parameter must have the same name.

Now when we try to fetch the particular employee (GET – {{url}}/employee/1) then for the first time application make a call to the database and store the result into the cache for given time (time-to-live).

We want to enabled caching for getOneEmployee(), updateEmployee(), and deleteEmployee().

@Service
public class EmployeeServiceImpl implements IEmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Override
    public Employee saveEmployee(Employee e) {
        return employeeRepository.save(e);
    }

    @Override
    @CachePut(value = "employees", key = "#empId")
    public Employee updateEmployee(Integer empId, Employee e) {
        Employee employee = getOneEmployee(empId);
        employee.setEmpName(e.getEmpName());
        employee.setEmpSal(e.getEmpSal());
        return employeeRepository.save(e);
    }

    @Override
    @CacheEvict(value = "employees", key = "#empId")
    public void deleteEmployee(Integer empId) {
        Employee employee = getOneEmployee(empId);
        employeeRepository.delete(employee);
    }

    @Override
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    @Cacheable(value = "employees", key = "#empId")
    public Employee getOneEmployee(Integer empId) {
        return employeeRepository.findById(empId)
                .orElseThrow(() -> new ResourceNotFoundException("Employee Not Found"));
    }

}

Assume if we have employee with child (like one-to-one, many-to-many, collection type) and we want to delete child also then we have to provide allEntries=true

@CacheEvict(value = "employees", allEntries = true)

We can use actuator to verify the caching:- http://localhost:8080/actuator/caches. Other endpoints of actuator can be found at http://localhost:8080/actuator.

If you enjoyed this post, share it with your friends. Do you want to share more information about the topic discussed above or do you find anything incorrect? Let us know in the comments. Thank you!

Leave a Comment

Your email address will not be published. Required fields are marked *