• Home
  • Java
  • Implementar Caching en una Aplicación Spring Boot
3.0 / 5

Implementar Caching en una Aplicación Spring Boot

0
3

Implementar Caching en una Aplicación Spring Boot

Hoy implementaré caching en una aplicación Spring Boot de una manera muy simple.

He tomado un ejemplo muy simple: almacenaré nombres de compuestos químicos y sus fórmulas científicas en una base de datos. Este puede ser un ejemplo perfecto donde podemos implementar caching.

¿Cuándo debería implementar caching?

Debería implementar caching en la capa de servicio que maneja datos que no se modifican con frecuencia. Datos como la fórmula de un compuesto químico casi nunca cambiarán o hay muy poca probabilidad de que lo hagan.

Implementación

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 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>3.2.5</version>
  <relativePath/>
 </parent>
 <groupId>com.caching.demo.app</groupId>
 <artifactId>caching-demo</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>caching-demo</name>
 <description>Demo project for Spring Boot Caching</description>
 <properties>
  <java.version>17</java.version>
 </properties>
 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <scope>runtime</scope>
  </dependency>
  <!--<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
  </dependency>-->
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</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-test</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
  </dependency>
 </dependencies>
 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>
</project>
Code language: Java (java)

Clase Principal:

package com.caching.demo.app;

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

@SpringBootApplication
@EnableCaching
public class CachingDemoApplication {
 public static void main(String[] args) {
  SpringApplication.run(CachingDemoApplication.class, args);
 }
}
Code language: Java (java)

Necesitamos agregar @EnableCaching para habilitar el caching en nuestra aplicación.

Entidad:

package com.caching.demo.app.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChemicalCompoundEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "chemical_name")
    private String ccName;

    @Column(name = "chemical_formula")
    private String ccFormula;
}
Code language: Java (java)

Esta es la entidad para los compuestos químicos. Almacena el nombre del compuesto químico como “Agua” en ccName y su fórmula H2O en ccFormula.

Repositorio:

package com.caching.demo.app.repository;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface CCRepository extends JpaRepository<ChemicalCompoundEntity, Long> {
    Optional<ChemicalCompoundEntity> findByCcName(String ccName);
    void deleteByCcName(String ccName);
}
Code language: Java (java)

Servicio:

package com.caching.demo.app.service;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import com.caching.demo.app.repository.CCRepository;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
@CacheConfig(cacheNames={"chemical_compounds"})
public class CCService {

    @Autowired
    private CCRepository repository;

    @CachePut(value = "chemical_compounds", key="#root.args[0]")
    public ChemicalCompoundEntity create(String ccName, String ccFormula) {
       return repository.save(ChemicalCompoundEntity.builder().ccName(ccName).ccFormula(ccFormula).build());
    }

    @Cacheable(value = "chemical_compounds", key="#root.args[0]")
    public ChemicalCompoundEntity getByName(String name) {
        return repository.findByCcName(name).get();
    }

    @CachePut(value = "chemical_compounds", key="#root.args[0]")
    public ChemicalCompoundEntity updateFormula(String ccName, String ccFormula) {
        Optional<ChemicalCompoundEntity> compound = repository.findByCcName(ccName);
        if(compound.isPresent()){
            ChemicalCompoundEntity chemComp = compound.get();
            chemComp.setCcFormula(ccFormula);
            return repository.save(chemComp);
        }
        return null;
    }

    @Transactional
    @CacheEvict(value="chemical_compounds", key="#root.args[0]")
    public void deleteCompound(String ccName) {
        repository.deleteByCcName(ccName);
    }

    @CacheEvict(value="chemical_compounds", allEntries = true, key="#root.args[0]")
    public void deleteAllCompound() {
        repository.deleteAll();
    }
}
Code language: Java (java)

Esta clase es interesante, ya que hemos utilizado muchas anotaciones:

  • @CacheConfig: Proporciona un mecanismo para compartir configuraciones relacionadas con el cache a nivel de clase. He creado un cache con el nombre: “chemical_compounds”.
  • @CachePut: El método con esta anotación siempre será llamado y el valor de retorno será almacenado en cache. Si el método devuelve void, no se almacenará nada en cache. Nunca devolverá el resultado del cache, siempre actualizará el cache existente. El atributo key="#root.args[0]" significa el primer argumento pasado al método. En mi caso, es el nombre químico. He usado esto como clave porque casi todos los métodos lo toman como parámetro y podemos identificarlo fácilmente de manera única por su nombre de compuesto. Mejor colocar esta anotación en métodos de creación/actualización.
//Cache solo el valor que se devuelve cuando comp es Agua
@CachePut(value="chemical_compounds", condition="#comp.name=='Water'")
public String getFormula(ChemicalCompoundEntity comp) {...}

//Si la longitud del resultado es menor a 64, no se almacenará en cache.
@CachePut(value="chemical_compounds", unless="#result.length()<64")
public String getFormula(ChemicalCompoundEntity comp) {...}
Code language: Java (java)
  • @Cacheable: Si el valor de retorno ya está presente en el cache, se devolverá sin ejecutar el método. Mejor mantener esta anotación en métodos que simplemente ejecutarán una consulta de selección. Si no se encuentra ningún valor en el cache para la clave calculada, se invocará el método objetivo y el valor devuelto se almacenará en el cache asociado. Tenga en cuenta que los tipos de retorno Optional se desenvuelven automáticamente.
  • @CacheEvict: Eliminará elementos del cache.

Internamente, @EnableCaching registrará ConcurrentMapCacheManager con el contenedor IOC.

Mi suposición es que el cache no es más que un mapa en memoria (puede ser un mapa concurrente) y por eso es importante pasar la clave para que los valores exactos se creen/actualicen/eliminan.

RestController:

package com.caching.demo.app.controller;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import com.caching.demo.app.service.CCService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class CCRestController {

    @Autowired
    private CCService ccService;

    @GetMapping("/create")
    public ResponseEntity<String> create(@RequestParam("name") String ccName, @RequestParam("formula") String ccFormula){
        ccService.create(ccName, ccFormula);
        return ResponseEntity.ok("Chemical Compound added");
    }

    @GetMapping("/get")
    public ResponseEntity<ChemicalCompoundEntity

> getChemical(@RequestParam("name") String name){
        return ResponseEntity.ok(ccService.getByName(name));
    }

    @PutMapping("/update")
    public ResponseEntity<String> updateFormula(@RequestParam("name") String ccName, @RequestParam("formula") String ccFormula){
        ccService.updateFormula(ccName, ccFormula);
        return ResponseEntity.ok("Updated");
    }

    @DeleteMapping("/delete")
    public ResponseEntity<String> delete(@RequestParam("name") String ccName){
        ccService.deleteCompound(ccName);
        return ResponseEntity.ok("Deleted");
    }

    @DeleteMapping("/delete-all")
    public ResponseEntity<String> deleteAll(){
        ccService.deleteAllCompound();
        return ResponseEntity.ok("All Deleted.");
    }
}
Code language: Java (java)

application.properties:

spring.application.name=caching-demo
server.port=9001

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Code language: YAML (yaml)

Salida:

Ejecute los siguientes endpoints GET uno por uno:

http://localhost:9001/create?name=Water&formula=H2O
http://localhost:9001/create?name=CarbonDiOxide&formula=CO2
http://localhost:9001/get?name=CarbonDiOxide
Code language: YAML (yaml)

Los primeros dos endpoints simplemente crearán nuevos datos en la base de datos y, además, los datos creados se mantendrán en cache porque estoy devolviendo la entidad guardada en los métodos y he colocado @CachePut sobre el método.

El tercer endpoint “get” no ejecutará sus métodos ya que el método de la capa de servicio está anotado con @Cacheable y el resultado será devuelto desde el cache. Los primeros dos endpoints crearon entradas en el cache. El último punto simplemente devuelve el resultado del cache si se encuentra, de lo contrario, irá y comprobará en la base de datos y luego devolverá y se colocará en cache también.

Logs: Solo se ejecutan dos sentencias de inserción y no se ejecuta ninguna consulta de selección:

Ahora, ejecute el siguiente endpoint PUT para actualizar la base de datos y el cache también:

Update La consulta se ejecutará y tanto la base de datos como el cache se actualizarán:

Ahora volveremos a ejecutar el endpoint “get” y esta vez obtendremos la fórmula actualizada del agua y no se ejecutará ninguna consulta de selección. Así es como nuestras interacciones con la base de datos se reducen y logramos mejoras en el rendimiento.

No se ejecuta ninguna consulta de selección según los logs.

De manera similar, si ejecuto los endpoints de eliminación, los valores se eliminarán tanto de la base de datos como del cache.

Un consejo sería nunca usar caching para métodos que devuelvan findAll() sin ningún límite.

Encuentra el código fuente del repositorio aquí.

Si encuentras algún error en cualquiera de mis artículos, por favor, coméntalo.

Gracias por leer este artículo.

Fuente aquí.

LEAVE YOUR COMMENTS