⚡ Mejora el rendimiento en Spring evitando consultas duplicadas con caché por solicitud


¿Te ha pasado que haces validaciones de seguridad con @PreAuthorize
, pero terminas llamando a la base de datos más de una vez por la misma entidad? 😱
Hoy te muestro cómo evitar eso elegantemente con un caché ligado al ciclo de vida de la petición en Spring Boot. Y lo mejor: ¡con ejemplos distintos!
🔁 El problema común
Mira este escenario con productos:
@PreAuthorize("@productoSecurity.tieneAcceso(#productoId, authentication.name)")
public Producto obtenerProducto(Long productoId) {
return productoRepository.findById(productoId).orElseThrow();
}
El método tieneAcceso
también usa findById
, ¡duplicando la consulta! 😩
💡 La solución: ThreadLocal como caché por petición
Creamos una clase genérica para guardar cualquier entidad durante la petición:
public class PeticionCache<T> {
private final ThreadLocal<Optional<T>> cache = new ThreadLocal<>();
public void guardar(T valor) {
cache.set(Optional.ofNullable(valor));
}
public Optional<T> obtener() {
return cache.get();
}
public void limpiar() {
cache.remove();
}
}
📦 Registro de cachés
Agrupamos las cachés por tipo de entidad en un solo bean inyectable:
@Component
@Getter
public class RegistroCache {
private final PeticionCache<Producto> producto = new PeticionCache<>();
public void limpiarTodo() {
producto.limpiar();
}
}
🔐 Seguridad que guarda en caché
En lugar de consultar dos veces, cacheamos al validar permisos:
@Component
@AllArgsConstructor
public class ProductoSecurity {
private final ProductoRepository repo;
private final RegistroCache registro;
public boolean tieneAcceso(Long id, String email) {
return repo.findById(id)
.filter(p -> p.getDueño().getEmail().equals(email))
.map(p -> {
registro.getProducto().guardar(p);
return true;
}).orElse(false);
}
}
🚀 Servicio que reutiliza la caché
Si la validación pasó, ¡la entidad ya está lista!
@PreAuthorize("@productoSecurity.tieneAcceso(#productoId, authentication.name)")
public Producto obtenerProducto(Long productoId) {
return registro.getProducto().obtener()
.orElseThrow(() -> new EntityNotFoundException("Producto no encontrado"));
}
🧼 Filtro para limpiar después de cada request
Evita fugas de memoria limpiando el caché al final de cada petición:
@Component
@RequiredArgsConstructor
public class FiltroLimpiezaCache extends OncePerRequestFilter {
private final RegistroCache registro;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
registro.limpiarTodo();
}
}
}
✅ Beneficios
- ❌ Evitas consultas duplicadas
- 🧼 Código más limpio
- 🔄 Reutilizable para distintas entidades
- ✅ Compatible con seguridad Spring
Si usas @PreAuthorize
, este patrón puede ahorrarte muchas líneas innecesarias y hacer tu app más rápida 🚀
Subscribe to my newsletter
Read articles from Carlos Exposito directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
