Spring Data JPA es un módulo muy útil provisto en Spring Framework (incluido Spring boot) para acceder a la capa de persistencia con el mínimo esfuerzo y reducir una gran cantidad de código repetitivo usando JPA. Permite crear consultas con diferentes enfoques, por ejemplo:
- Métodos de consulta derivada (la creación de la consulta basado en el nombre de los métodos)
- Anotación @Query (escribiendo consultas JPQL o SQL nativas).
Además de esos enfoques comunes, existe una forma de crear consultas de forma dinámica mediante el patrón de especificación ya proporcionado por Spring JPA y aprovechando el “Criteria API” de JPA.
¿Qué es el patrón Especificación?
Dado un objeto, a veces es necesario aplicar diferentes condiciones a una consulta, lo que requiere crear muchos métodos diferentes para cada combinación posible. El patrón de especificación se deriva de los conceptos presentados en el libro de diseño basado en dominios de Eric Evans. Proporciona un patrón de diseño que nos permite separar los criterios de búsqueda del objeto que realiza la búsqueda. Veamos un ejemplo.
Hay un grupo de criaturas diferentes y, a menudo, necesitamos seleccionar algún subconjunto de ellas. Podemos escribir nuestra especificación de búsqueda como “criaturas que pueden volar”, “criaturas que pesen más de 500 kilogramos”, o como una combinación de otras especificaciones de búsqueda, y luego dársela a la parte que realizará el filtrado.
¿Por qué usar el patrón Especificación?
Usando el patrón de repositorio proporcionado por Spring Data JPA y sus capacidades, es común comenzar a agregar nuevas definiciones de métodos para cada consulta diferente que necesita en la aplicación y la lógica del negocio. Para las entidades con muchos atributos/campos, el Repositorio podría terminar con una combinación diferente de consultas requeridas, todas ellas en un método separado, por lo tanto, nuestra clase crecerá y contendrá decenas de métodos – he visto casos de clases con casi 100 métodos –
interface ProductRepository extends JpaRepository<Product, String>{ Optional<Product> findById(long id); List<Product> findAllByNameLike(String name); List<Product> findAllByNameLikeAndPriceLessThanEqual(String name, Double price); List<Product> findAllByCategoryInAndPriceLessThanEqual(List<Category> categories, Double price); List<Product> findAllByCategoryInAndPriceBetween(List<Category> categories, Double bottom,Double top); List<Product> findAllByNameLikeAndCategoryIn(String name, List<Category> categories); List<Product> findAllByNameLikeAndCategoryInAndPriceBetween(String name, List<Category> categories, Double bottom, Double top); // more methods ..... List<Product> findAllByCategoryInAndPriceGreaterThanEqual(List<Category> categories, Double price); ... //more methods List<Product> findAllByNameLikeAndCategoryInAndPriceBetweenAndManufacturingPlace_State( String name, List<Category> categories, Double bottom, Double top, STATE state); }
En términos de productividad, ese escenario está bien, como desarrollador puedo crear en unos segundos un método que acceda a la base de datos filtrando por algunos campos específicos y devolviendo valores en Java, y nosotros como desarrolladores nos centraremos en las características y la lógica del negocio. Sin embargo, cuando se trata de legibilidad y mantenibilidad, ese escenario, una clase con decenas (por no decir casi cientos de métodos) es una pesadilla. Y debido a las convenciones de nomenclatura de Spring Data JPA, es posible que tengamos métodos con nombres incomprensibles.
List<Product> findAllByNameLikeAndCategoryInAndPriceBetweenAndManufacturingPlace_State( String name, List<Category> categories, Double bottom, Double top, STATE state);
Por esa razón el patrón de especificación es una buena solución para mejorar la legibilidad y la capacidad de mantenimiento de nuestro código, con un mínimo de código y reutilizando el código existente.
Usando el patrón Especificación en Spring Data JPA
Spring ya tiene una interfaz Specification
para implementarla y hacer que las diferentes especificaciones sean re-utilizables en nuestro código. Como podemos ver, se basa en el “Criteria API” de JPA.
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }
Creando las specifications propias
Luego podemos crear todas las diferentes especificaciones personalizadas en función de lo que necesitamos en las consultas que nuestra aplicación necesita. Afortunadamente, podemos usar expresiones Lambda para simplificar la implementación de cada especificación.
public class CustomerSpecifications { public static Specification<Product> belongsToCategory(List<Category> categories){ return (root, query, criteriaBuilder)-> criteriaBuilder.in(root.get(Product_.CATEGORY)).value(categories); public static Specification<Product> priceGreaterThan(double price){ return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get(Product_.PRICE), price); } public static Specification<Product> nameEquals(String name){ return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(Product_.NAME), name); } public static Specification<Product> expired(){ return (root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Product_.EXPIRATION_DATE), LocalDate.now()); } }
La interfaz de especificación también tiene los métodos auxiliares public static and()
, or()
y where()
que nos permiten combinar múltiples especificaciones. También proporciona un método not()
que nos permite negar una especificación.
Usando las Specifications propias
Al implementar la interfaz JpaSpecificationExecutor
en la clase Repository, el repositorio ahora tiene algunos métodos para consultar usando especificaciones. Esos métodos reemplazarán el grupo de métodos para cada combinación de criterios que necesitamos.
List<T> findAll(Specification<T> spec); Page<T> findAll(Specification<T> spec, Pageable pageable); List<T> findAll(Specification<T> spec, Sort sort);
Por lo cual solo debemos de implementar/extender dicha interfaz JpaSpecificationExecutor
en nuestro repositorio.
public interface ProductRepository extends JpaRepository<Product>, JpaSpecificationExecutor { // Your query methods here }
Y luego podemos usar nuestras especificaciones propias con el método findAll
del repository.
import static CustomerSpecifications.*; private ProductRepository productRepository; public List<Product> getPremiumProducts(String name, List<Category> categories) { return productRepository.findAll( where(belongsToCategory(categories)) .and(nameLike(name)) .and(isPremium()) .not(expired())); }
Podemos reutilizar las especificaciones personalizadas creadas en diferentes partes de la lógica de negocio y combinarlas según las necesidades de ese requisito mediante las operaciones AND, OR, NOT, WHERE.
¿Cuándo debo usar Specification en lugar de métodos Query?
Depende de la característica de cada aplicación, si las entidades tienen pocos campos y la combinación de diferentes consultas para esos campos es pequeña, entonces es totalmente aceptable usar la manera estándar de Spring Data JPA (consultas derivadas del nombre del método o anotación @Query
), al final el código es mantenible y legible y este modo de desarrollo acelera nuestra productividad para entregar y satisfacer las necesidades de la aplicación.
Sin embargo, si las entidades en la aplicación tienen varios campos, y en la práctica estamos haciendo varias consultas diferentes por diferentes campos, seguramente terminaremos con un Repositorio lleno de métodos de consulta (decenas) y algunos de ellos con nombres incomprensibles (debido a la convención de nomenclatura de Spring Data JPA). Por tanto, nos encontramos ante un buen candidato para implementar el Patrón de Especificación, y finalmente mejorar la legibilidad y mantenibilidad del código.
Conclusión
El patrón de especificación no es adecuado para todos los casos, requiere algo de esfuerzo para crear las especificaciones personalizadas y, por supuesto, para muchas aplicaciones, el enfoque de usar Query derivado del nombre del método o la anotación @Query
es lo suficientemente bueno. E implementar este patrón podría verse como excesivo e innecesario.
Pero, hay una serie de aplicaciones donde por sus características la cantidad de consultas puede crecer afectando la legibilidad y mantenibilidad del código, y el Patrón de Especificación les ayudará a reducir y reutilizar el código. Usando Spring JPA Data, podemos aprovechar el patrón de especificación con Criteria API que ya es proporcionado.
Finalmente, la intención de este artículo es que pueda ayudarle a quienes se enfrentan con un caso donde el patrón de Especificación en Spring Data es un buen candidato a solución.
Referencias
- https://reflectoring.io/spring-data-specifications/
- https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
- https://medium.com/geekculture/spring-data-jpa-a-generic-specification-query-language-a599aea84856
- https://java-design-patterns.com/patterns/specification/
cuándo es que la clase Product_ es generada en el target?
Hola Alejandro…
En este ejemplo se esta usando el JPA Metamodel, esto se hace para que sea 100% tipado, y no dependa de estar poniendo nombres de columnas o atributos en String, que luego pueden cambiar en el futuro.
Para generar estas clases es necesario agregar la siguiente dependencia maven.
groupId:org.hibernate
artifactId:hibernate-jpamodelgen