Spring Boot— CRUD example with Caching

Buddhi Prabhath
6 min readApr 4, 2019

--

https://github.com/buddhiprab/spring-boot-crud-cache.git

How to configure spring cache in Service methods in conjunction with @Cacheable, @CacheEvict annotations, so that when a record added/updated (write) to database it will be reflected in the caches of findAll, findById (read) methods

i’m using a delivery record example to illustrate the cashing behaviour.

A. findAll() method caching behaviour

  1. first call to the findAll method: “deliveries” cache will miss, database hit
  2. second call onwards: “deliveries” cache will hit, no database hit

B. saveOrUpdate() method in conjunction with findAll, findById caching behaviour

  1. call to the saveOrUpdate method: will do a save or update a delivery record to database and

@Caching(evict = {
@CacheEvict(value=”delivery”, allEntries=true),
@CacheEvict(value=”deliveries”, allEntries=true) })

will cause “deliveries” and “delivery” caches to be cleared

2. now call to findAll or findById method: cache will miss and database will hit because the saveOrUpdate() method cleared the caches, so the new records added to the db table will be fetched and cache will be updated with new records fetched from db

Sample code to demonstrate caching behaviour

Service class

  • note the @Cacheable, @CacheEvict annotations, responsible for caching and cache clearing
package com.buddhi.service;

import com.buddhi.dto.DeliveryDto;
import com.buddhi.model.Delivery;
import com.buddhi.repository.DeliveryRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.beans.BeanUtils.copyProperties;

@Slf4j
@Service
public class DeliveryService {
@Autowired
DeliveryRepository deliveryRepository;

@Cacheable("deliveries")
public List<DeliveryDto> findAll(){
log.info("DeliveryService: findAll");
List<Delivery> deliveries = deliveryRepository.findAll();
List<DeliveryDto> deliveryDtos = new ArrayList<>();
for (Delivery delivery:deliveries){
DeliveryDto deliveryDto = new DeliveryDto();
copyProperties(delivery, deliveryDto);
deliveryDtos.add(deliveryDto);
}
return deliveryDtos;
}

@Cacheable("delivery")
public DeliveryDto findById(Long id){
log.info("DeliveryService: findById");
Delivery delivery = deliveryRepository.findById(id).orElse(null);
DeliveryDto deliveryDto = new DeliveryDto();
copyProperties(delivery,deliveryDto);
return deliveryDto;
}

@Caching(evict = {
@CacheEvict(value="delivery", allEntries=true),
@CacheEvict(value="deliveries", allEntries=true)})
public Delivery saveOrUpdate(DeliveryDto deliveryDto){
log.info("DeliveryService: saveOrUpdate, {}", deliveryDto.getPickupName());
Delivery delivery = Delivery.builder()
.pickupName(deliveryDto.getPickupName())
...
.build();
if(deliveryDto.getId()!=null){
delivery.setId(deliveryDto.getId());
}
return deliveryRepository.save(delivery);
}
}

Repository class

package com.buddhi.repository;

import com.buddhi.model.Delivery;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface DeliveryRepository extends JpaRepository<Delivery, Long> {
}

Controller class

package com.buddhi.controller;

import com.buddhi.dto.DeliveryDto;
import com.buddhi.model.Delivery;
import com.buddhi.service.DeliveryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequestMapping(path = "/deliveries")
public class DeliveryController {
@Autowired
DeliveryService deliveryService;

@PostMapping(path = "")
public ResponseEntity<Long> createOrUpdateDelivery(@RequestBody DeliveryDto deliveryDto) {
log.info("DeliveryController: createOrUpdateDelivery");
Delivery delivery = deliveryService.saveOrUpdate(deliveryDto);
return new ResponseEntity<>(delivery.getId(), HttpStatus.OK);
}

@GetMapping(path = "")
public ResponseEntity<List<DeliveryDto>> getDeliveries() {
log.info("DeliveryController: getDeliveries");
List<DeliveryDto> deliveries = deliveryService.findAll();
return new ResponseEntity<>(deliveries, HttpStatus.OK);
}

@GetMapping(path = "/{id}")
public ResponseEntity<DeliveryDto> getDelivery(@PathVariable Long id) {
log.info("DeliveryController: getDelivery");
DeliveryDto delivery = deliveryService.findById(id);
return new ResponseEntity<>(delivery, HttpStatus.OK);
}
}

Dto class

package com.buddhi.dto;

import com.buddhi.util.CustomDateAndTimeDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Data;

import java.util.Date;

@Data
public class DeliveryDto {
private Long id;
//pickup
private String pickupName;
private String pickupAddress;
@JsonDeserialize(using= CustomDateAndTimeDeserialize.class)
private Date pickupDateTime;
private String[] pickupContactNumbers;
private String pickupComment;
//drop
private String dropName;
private String dropAddress;
private String[] dropContactNumbers;
private String dropComment;
}

Model class

package com.buddhi.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.Date;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Builder
@Table(name = "delivery")
public class Delivery {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "pickup_name")
private String pickupName;
@Column(name = "pickup_address")
private String pickupAddress;
@Column(name = "pickup_datetime")
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date pickupDateTime;
@Column(name = "pickup_contact_numbers")
private String pickupContactNumbers;
@Column(name = "pickup_comment")
private String pickupComment;
@Column(name = "drop_name")
private String dropName;
@Column(name = "drop_address")
private String dropAddress;
@Column(name = "drop_contact_numbers")
private String dropContactNumbers;
@Column(name = "drop_comment")
private String dropComment;
}

SpringBoot Application class

package com.buddhi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class DeliveryApplication {

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

}

application.yml

server:
port: 8080

spring:
h2:
console:
enabled: true
datasource
:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb
username: sa
password: password
platform: org.hibernate.dialect.Oracle10gDialect
continue-on-error: true
jpa
:
hibernate:
ddl-auto: none

logging:
level:
root: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- [%15.15t] %-40.40logger{39} :: %X{correlationId} : %m%n%ex{full}"

pom.xml

<?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 http://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.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from delivery -->
</parent>
<groupId>com</groupId>
<artifactId>buddhi</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>buddhi</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-cache</artifactId>
</dependency>
<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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

This Interceptor is to add a tracing id to log, so we can trace the individual requests in the log(this has nothing to do with caching)

package com.buddhi.config;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Component
public class CorrelationInterceptor extends HandlerInterceptorAdapter {
private static final String CORRELATION_ID_HEADER_NAME = "X-Correlation-Id";
private static final String CORRELATION_ID_LOG_VAR_NAME = "correlationId";
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) throws Exception {
final String correlationId = getCorrelationIdFromHeader(request);
MDC.put(CORRELATION_ID_LOG_VAR_NAME, correlationId);
return true;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) throws Exception {
MDC.remove(CORRELATION_ID_LOG_VAR_NAME);
}
private String getCorrelationIdFromHeader(final HttpServletRequest request) {
String correlationId = request.getHeader(CORRELATION_ID_HEADER_NAME);
if (StringUtils.isBlank(correlationId)) {
correlationId = generateUniqueCorrelationId();
}
return correlationId;
}
private String generateUniqueCorrelationId() {
return UUID.randomUUID().toString();
}
}

unique id inserted for request tracing in the log (correlationId variable added in application.yml logging pattern)

add the interceptor in WebMvcConfigurer

package com.buddhi.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private CorrelationInterceptor correlationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(correlationInterceptor);
}
}

folder structure

CustomDateAndTimeDeserialize class

package com.buddhi.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
public class CustomDateAndTimeDeserialize extends JsonDeserializer<Date> {
private SimpleDateFormat dateFormat = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");

@Override
public Date deserialize(JsonParser paramJsonParser,
DeserializationContext paramDeserializationContext)
throws IOException, JsonProcessingException {
String str = paramJsonParser.getText().trim();
try {
return dateFormat.parse(str);
} catch (ParseException e) {
log.error(e.getMessage());
e.printStackTrace();
}
return paramDeserializationContext.parseDate(str);
}
}

schema.sql

create table if not exists delivery
(
id int auto_increment,
pickup_name varchar(100) null,
pickup_address varchar(200) null,
pickup_datetime datetime null,
pickup_contact_numbers varchar(100) null,
pickup_comment text null,
drop_name varchar(100) null,
drop_address varchar(200) null,
drop_contact_numbers varchar(45) null,
drop_comment text null,
primary key (id)
);

create index idx_pdt on delivery (pickup_datetime);

postman requests

1: save 1 record, save to db and will clear caches

2: get all, will return the saved records, get all from db

3: get all again, will get records from cache, no service method hit(note the service log line is not there, only controller line) so no db hit

3: save 2nd record, save to db and will clear caches

4: get all will return 2 records, get all from db

5: save 3rd record, save to db and will clear cache

6: get all will return 3 records, will get from db

7: update 2nd record, will save to db and clear caches

8: get all will return the updated 2nd record, will get all from db

9: call get all again, will get all from cache

--

--