El patrón de diseño Specification, ejemplo de implementación y uso en JPA con Spring Data

Escrito por picodotdev el .
java planeta-codigo
Enlace permanente Comentarios

Los métodos de búsqueda y consulta permiten recuperar objetos de las bases de datos según los criterios deseados. Dependiendo del tamaño de la aplicación y sus casos de uso el número de consultas será más o menos grande. Con un número de consultas grande estas se vuelven complejas de mantener y generan duplicación de lógica de negocio. Para simplificar el mantenimiento de un número grande de consultas y evitar duplicidad de lógica de negocio una solución es implementar el patrón de diseño Specification.

Java

Spring

Dado un objeto suele ser necesario comprobar si cumple una o más condiciones. Estas condiciones pueden implementarse tanto en un método del objeto como en la lógica de persistencia en la base de datos.

Esta aproximación tiene dos inconvenientes, el número de métodos de consulta crece significativamente en las aplicaciones grandes y las consultas son conjunto fijo sin posibilidad de extensión salvo añadir nuevos métodos, las consultas no son fáciles de externalizar y reutilizar.

En estos casos implementar el patrón de diseño Specification ayuda a hacer el código más mantenible, extensible, simple y de más fácil lectura.

Los siguientes ejemplos implementan el patrón Specification para comprobar si un objeto cumple una serie de condiciones de negocio y como Spring Data hace uso del patrón para construir las condiciones de las consultas de JPA. Los ejemplos incluyen teses que usan la herramienta TestConainers para hacer pruebas de integración en Java con la base de datos PostgreSQL en un contenedor Docker.

El problema en las consultas

Suponiendo que se tiene la siguiente entidad del dominio con una serie de campos la idea primera y más directa para implementar si un producto cumple una serie de condiciones es añadir métodos en las clases, un método por cada condición. Por ejemplo, para buscar los productos que que son baratos, tienen un tiempo largo de existencia o un sobrestock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package io.github.picodotdev.blogbitix.patronspecification.domain.product;

...

@Entity
@Table(name = "Product")
public class Product implements Specificable<Product> {

    ...

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private LocalDate date;
    private BigDecimal price;
    private Integer units;

    public Product() {
    }

    public Product(String name, LocalDate date, BigDecimal price, Integer units) {
        this.name = name;
        this.date = date;
        this.price = price;
        this.units = units;
    }

    ...

    public boolean isCheap() {
        return ...;
    }

    public boolean isLongTerm() {
        return ...;
    }

    public boolean isOverstock() {
        return ...;
    }
}
Product-1.java

Esta aproximación sencilla de implementar y suficiente en aplicaciones pequeñas tiene dos inconvenientes. El número de métodos a escribir crece significativamente para aplicaciones grandes o complejas y los criterios de los métodos de consulta son fijos, no son extensibles. Para solventar estos dos problemas se opta por crear métodos con los criterios individuales y se combinan entre ellos dinámicamente para obtener la consulta deseada.

Aquí es donde el patrón de diseño Specification es de utilidad. Este patrón también es aplicable a las consultas presentes en las clases repositorio de acceso a la base de datos donde seguramente es más probable repetir la misma lógica de condiciones en varias consultas hardcodeado en las SQLs. Con los mismos problemas, condiciones repetidas en varios métodos y proliferación de métodos de consulta. Esta es la razón de que Spring Data implemente el patrón Specification.

Qué es y ventajas del patrón de diseño Specification

El patrón de diseño Specification permite encapsular una pieza del conocimiento del dominio y rehusarla en diferentes partes de la aplicación. Utilizándolo se mueven estas reglas de negocio a clases llamadas specifications.

El patrón de diseño Specification parte de una interfaz con un método a implementar para encapsular la lógica de negocio que comprueba si la condición se cumple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.patronspecification.specification;

public interface Specification<T> {

    boolean isSatisfied(T object);

    default Specification<T> and(Specification<T>... specifications) {
        return new AndSpecification(specifications);
    }

    default Specification<T> or(Specification<T>... specifications) {
        return new OrSpecification(specifications);
    }

    default Specification<T> not(Specification<T> specification) {
        return new NotSpecification(specification);
    }
}
Specification.java

Por cada condición hay una implementación de la interfaz.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specifications;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class IsCheapSpecification implements Specification<Product>, org.springframework.data.jpa.domain.Specification<Product> {

    private String priceAttributeName;

    public IsCheapSpecification() {
        this(null) ;
    }

    public IsCheapSpecification(String path) {
        this.priceAttributeName = Specifications.getAttributeName(path, "price");
    }

    @Override
    public boolean isSatisfied(Product product) {
       return Product.CHEAP_PRICE.compareTo(product.getPrice()) == 1;
    }

    @Override
    public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.lt(root.get(priceAttributeName), Product.CHEAP_PRICE);
    }
}
IsCheapSpecification.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specifications;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.time.LocalDate;
import java.time.Period;

public class IsLongTermSpecification implements Specification<Product>, org.springframework.data.jpa.domain.Specification<Product> {

    private String dateAttributeName;

    public IsLongTermSpecification() {
        this(null) ;
    }

    public IsLongTermSpecification(String path) {
        this.dateAttributeName = Specifications.getAttributeName(path, "date");
    }

    @Override
    public boolean isSatisfied(Product product) {
       return !Period.between(product.getDate(), LocalDate.now()).minus(Product.LONG_TERM_PERIOD).isNegative();
    }

    @Override
    public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        LocalDate longTermDate = LocalDate.now().minus(Product.LONG_TERM_PERIOD);
        return criteriaBuilder.lessThan(root.get(dateAttributeName), longTermDate);
    }
}
IsLongTermSpecification.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specifications;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class IsOverstockSpecification implements Specification<Product>, org.springframework.data.jpa.domain.Specification<Product> {

    private String priceAttributeName;

    public IsOverstockSpecification() {
        this(null) ;
    }

    public IsOverstockSpecification(String path) {
        this.priceAttributeName = Specifications.getAttributeName(path, "units");
    }

    @Override
    public boolean isSatisfied(Product product) {
       return product.getUnits() > Product.OVERSTOCK_UNITS;
    }

    @Override
    public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.gt(root.get(priceAttributeName), Product.OVERSTOCK_UNITS);
    }
}
IsOverstockSpecification.java

En el objeto Product se implementa el patrón Visitor con la interfaz Specificable donde cada implementación de la clase Specification trata la lógica y la clase Product solo tiene el método satisfies que invoca a la instancia de specification recibida como parámetro.

1
2
3
4
5
6
package io.github.picodotdev.blogbitix.patronspecification.specification;

public interface Specificable<T> {

    boolean satisfies(Specification<T> object);
}
Specificable.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package io.github.picodotdev.blogbitix.patronspecification.domain.product;

import io.github.picodotdev.blogbitix.patronspecification.specification.Specificable;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;

...

@Entity
@Table(name = "Product")
public class Product implements Specificable<Product> {

    ...

    @Override
    public boolean satisfies(Specification<Product> specification) {
        return specification.isSatisfied(this);
    }

    ...
}
Product.java

Para realizar combinaciones con operaciones lógicas and, or o not se utiliza el patrón Composite. De entre las operaciones básicas solo se muestra la operación equals, sería neesario implementar otro tipo de operaciones como lessThan, greaterThan, contains u otras si es necesario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specifications;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.beans.PropertyDescriptor;

public class EqualsSpecification<T> implements Specification<T>, org.springframework.data.jpa.domain.Specification<T> {

    private String property;
    private Object value;
    private String propertyAttributeName;

    public EqualsSpecification(String property, Object value) {
        this(property, value, null);
    }

    public EqualsSpecification(String property, Object value, String path) {
        this.property = property;
        this.value = value;
        this.propertyAttributeName = Specifications.getAttributeName(path, property);
    }

    @Override
    public boolean isSatisfied(T product) {
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(property, Product.class);
            Object v = descriptor.getReadMethod().invoke(product);
            if (v == value) {
                return true;
            }
            if (v != null && value == null || v == null && value != null) {
                return false;
            }
            return value.equals(v);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.equal(root.get(propertyAttributeName), value);
    }
}
EqualsSpecification.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.patronspecification.specification;

import java.util.Arrays;
import java.util.List;

public class AndSpecification<T> implements Specification<T> {

    private List<Specification<T>> specifications;

    public AndSpecification(Specification<T>... specifications) {
        this.specifications = Arrays.asList(specifications);
    }

    public boolean isSatisfied(T object) {
        return specifications.stream().allMatch(s -> { return s.isSatisfied(object); });
    }
}
AndSpecification.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.patronspecification.specification;

import java.util.Arrays;
import java.util.List;

public class OrSpecification<T> implements Specification<T> {

    private List<Specification<T>> specifications;

    public OrSpecification(Specification<T>... specifications) {
        this.specifications = Arrays.asList(specifications);
    }

    public boolean isSatisfied(T object) {
        return specifications.stream().anyMatch(s -> { return s.isSatisfied(object); });
    }
}
OrSpecification.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.patronspecification.specification;

public class NotSpecification<T> implements Specification<T> {

    private Specification<T> specification;

    public NotSpecification(Specification<T> specification) {
        this.specification = specification;
    }

    public boolean isSatisfied(T object) {
        return !specification.isSatisfied(object);
    }
}
NotSpecification.java

Al implementar el patrón Specification se hace uso de varios patrones:

  • El patrón de diseño Visitor, en el método satisfies de la clase Product realmente se llama al método isSatisfied de la interfaz Specification.
  • El patrón de diseño Composite en las operaciones lógicas and, or y not. Estas condiciones lógicas de agrupación se componente de otras independientemente de contengan una o varias.
  • El patrón de diseño Comamnd, se construye la instancia de Specification a ejecutar y el método satisfies lo ejecuta.
  • El patrón de diseño Builder se utiliza para facilitar la construcción de las condiciones con una API fluida y ocultar las clases concretas que implementan la interfaz Specification. Spring Data lo implementa.

La siguiente prueba unitaria muestra con código el uso del patrón Specification.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import java.math.BigDecimal;
import java.time.LocalDate;

import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.specification.AndSpecification;
import io.github.picodotdev.blogbitix.patronspecification.specification.NotSpecification;
import io.github.picodotdev.blogbitix.patronspecification.specification.OrSpecification;
import io.github.picodotdev.blogbitix.patronspecification.specification.Specification;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest
@ContextConfiguration(initializers = { ProductSpecificationTest.Initializer.class })
public class ProductSpecificationTest {

    @Test
    void testEqualsSpecification() {
        // given
        Product product = new Product("Raspberry Pi", LocalDate.now(), new BigDecimal("80.0"), 10);

        // and
        Specification<Product> specification = new EqualsSpecification("name", "Raspberry Pi");

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testIsCheapSpecification() {
        // given
        Product product = new Product("Pin", LocalDate.now(),new BigDecimal("1.0"), 50);

        // and
        Specification<Product> specification = new IsCheapSpecification();

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testIsLongTermSpecification() {
        // given
        Product product = new Product("Raspberry Pi", LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),new BigDecimal("80.0"), 10);

        // and
        Specification<Product> specification = new IsLongTermSpecification();

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testIsOverstockSpecification() {
        // given
        Product product = new Product("Pin", LocalDate.now(),new BigDecimal("5.0"), 50);

        // and
        Specification<Product> specification = new IsOverstockSpecification();

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testIsCheapAndIsLongTermSpecification() {
        // given
        Product product = new Product("Pin", LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),new BigDecimal("1.0"), 50);

        // and
        Specification<Product> cheapSpecification = new IsCheapSpecification();
        Specification<Product> longTermSpecification = new IsLongTermSpecification();
        Specification<Product> specification = new AndSpecification<>(cheapSpecification, longTermSpecification);

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testAndSpecification() {
        // given
        Product product = new Product("Raspberry Pi",LocalDate.now(), new BigDecimal("80.0"), 10);

        // and
        Specification<Product> specification = new AndSpecification<>(new EqualsSpecification("name", "Raspberry Pi"), new EqualsSpecification("price", new BigDecimal("80.0")), new EqualsSpecification("units", 10));

        // then
        assertTrue(product.satisfies(specification));
    }

    @Test
    void testOrSpecification() {
        // given
        Product product = new Product("Raspberry Pi", LocalDate.now(), new BigDecimal("80.0"), 50);

        // and
        Specification<Product> specificationA = new OrSpecification<>(new EqualsSpecification("name", "Raspberry Pi"), new EqualsSpecification("price", new BigDecimal("1.0")), new EqualsSpecification("units", 0));
        Specification<Product> specificationB = new OrSpecification<>(new EqualsSpecification("name", ""), new EqualsSpecification("price", new BigDecimal("80.0")), new EqualsSpecification("units", 0));
        Specification<Product> specificationC = new OrSpecification<>(new EqualsSpecification("name", ""), new EqualsSpecification("price", new BigDecimal("0.0")), new EqualsSpecification("units", 50));
        Specification<Product> specificationZ = new OrSpecification<>(new EqualsSpecification("name", ""), new EqualsSpecification("price", new BigDecimal("0.0")), new EqualsSpecification("units", 0));

        // then
        assertTrue(product.satisfies(specificationA));
        assertTrue(product.satisfies(specificationB));
        assertTrue(product.satisfies(specificationC));
        assertFalse(product.satisfies(specificationZ));
    }

    @Test
    void testNotSpecification() {
        // given
        Product product = new Product("Raspberry Pi", LocalDate.now(), new BigDecimal("80.0"), 10);

        // and
        Specification<Product> specification = new NotSpecification<>(new AndSpecification<Product>(new EqualsSpecification("name", "Raspberry Pi"), new EqualsSpecification("price", new BigDecimal("80.0")), new EqualsSpecification("units", 10)));

        // then
        assertFalse(product.satisfies(specification));
    }

    public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of("spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration")
                    .applyTo(configurableApplicationContext.getEnvironment());
        }
    }
}
ProductSpecificationTest.java

Implementación para JPA con Spring Data

El proyecto Spring Data para el acceso a bases de datos con JPA implementa el patrón de diseño Specification, la interfaz JpaSpecificationExecutor añade a los repositorios métodos de búsqueda que reciben un argumento de tipo Specification.

Esta clase Specification transforma las condiciones en un objeto Predicate que es el que JPA usa para las condiciones de la consulta SQL que se genera. La interfaz JpaSpecificationExecutor también añade métodos para hacer búsquedas paginadas y con ordenación.

Si en el proyecto se utiliza Spring y JPA esta es la opción recomendada, si no se utiliza Spring o se utiliza otra librería de persistencia distinta a JPA se puede realizar una implementación siguiendo los principios del patrón Specification.

Las clases EqualsSpecification, IsCheapSpecification, IsLongTermSpecification, y IsOverstockSpecification anteriores también implementan la interfaz Specification de Spring Data. Estas clases implmentan dos interfaces distintas para diferentes cosas, para hacer comprobaciones sobre un objeto en memoria y para generar clases Predicate con las condiciones equivalentes de JPA, son símplemente ejemplos y para separar conceptos no estaría mal dividir cada clase en dos para que implementen las interfaces de forma individual.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.patronspecification.domain.product;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ProductRepository extends PagingAndSortingRepository<Product, Long>, JpaSpecificationExecutor<Product> {

    @Override
    @Modifying
    @Query("delete from Product")
    void deleteAll();
}
ProductRepository.java

La siguiente prueba de integración con Testcontainers, PostgresSQL y Docker prueba el repositorio con las implementaciones de las clases del patrón Specification para JPA de Spring Data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package io.github.picodotdev.blogbitix.patronspecification.domain.product.specification;

import io.github.picodotdev.blogbitix.patronspecification.DefaultPostgresContainer;
import io.github.picodotdev.blogbitix.patronspecification.domain.product.Product;
import io.github.picodotdev.blogbitix.patronspecification.domain.product.ProductRepository;
import liquibase.pro.packaged.T;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.jdbc.Sql;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@ContextConfiguration(initializers = { DefaultPostgresContainer.Initializer.class })
public class ProductJpaSpecificationTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    @Sql("/sql/products.sql")
    void testIsCheapSpecification() {
        // given
        Specification<Product> specification = new IsCheapSpecification();

        // then
        assertEquals(1, productRepository.findAll(specification).size());
    }

    @Test
    @Sql("/sql/products.sql")
    void testIsLongTermSpecification() {
        // given
        Specification<Product> specification = new IsLongTermSpecification();

        // then
        assertEquals(1, productRepository.findAll(specification).size());
    }

    @Test
    @Sql("/sql/products.sql")
    void testIsOverstockSpecification() {
        // given
        Specification<Product> specification = new IsOverstockSpecification();

        // then
        assertEquals(1, productRepository.findAll(specification).size());
    }

    @Test
    @Sql("/sql/products.sql")
    void testAndSpecification() {
        // given
        Specification<Product> specification = new EqualsSpecification("name", "Raspberry Pi").and(new EqualsSpecification("price", new BigDecimal("80.0"))).and(new EqualsSpecification("units", 10));

        // then
        assertEquals(1, productRepository.findAll(specification).size());
    }

    @Test
    @Sql("/sql/products.sql")
    void testOrSpecification() {
        // given
        Specification<Product> specificationA = new EqualsSpecification("name", "Raspberry Pi").or(new EqualsSpecification("price", new BigDecimal("0.0"))).or(new EqualsSpecification("units", 0));
        Specification<Product> specificationB = new EqualsSpecification("name", "").or(new EqualsSpecification("price", new BigDecimal("80.0"))).or(new EqualsSpecification("units", 0));
        Specification<Product> specificationC = new EqualsSpecification("name", "").or(new EqualsSpecification("price", new BigDecimal("0.0"))).or(new EqualsSpecification("units", 50));
        Specification<Product> specificationZ = new EqualsSpecification("name", "").or(new EqualsSpecification("price", new BigDecimal("0.0"))).or(new EqualsSpecification("units", 0));

        // then
        assertEquals(1, productRepository.findAll(specificationA).size());
        assertEquals(1, productRepository.findAll(specificationB).size());
        assertEquals(1, productRepository.findAll(specificationC).size());
        assertEquals(0, productRepository.findAll(specificationZ).size());
    }

    @Test
    @Sql("/sql/products.sql")
    void testNotSpecification() {
        // given
        Specification<Product> specification = Specification.not(new EqualsSpecification("name", "Raspberry Pi").and(new EqualsSpecification("price", new BigDecimal("80.0"))).and(new EqualsSpecification("units", 50)));

        // then
        assertEquals(7, productRepository.findAll(specification).size());
    }

    private <T> Specification<T> equalsSpecification(String property, Object value) {
        return (root, query, criteriaBuilder) -> {
            return criteriaBuilder.equal(root.get("name"), "Raspberry Pi");
        };
    }
}
ProductJpaSpecificationTest.java
1
2
3
4
5
6
7
8
delete from Product;
insert into Product (id, name, date, price, units) values (1, 'Pin', to_date('2010/01/01','YYYY/MM/DD'), 1.00, 50);
insert into Product (id, name, date, price, units) values (2, 'Raspberry Pi', CURRENT_DATE, 80.00, 10);
insert into Product (id, name, date, price, units) values (3, 'Intel NUC', CURRENT_DATE, 400.00, 10);
insert into Product (id, name, date, price, units) values (4, 'PlayStation 4', CURRENT_DATE, 350.00, 10);
insert into Product (id, name, date, price, units) values (5, 'BenQ', CURRENT_DATE, 330.00, 10);
insert into Product (id, name, date, price, units) values (6, 'Amazon Kindle', CURRENT_DATE, 130.00, 10);
insert into Product (id, name, date, price, units) values (7, 'Fleck Duo 7 50', CURRENT_DATE, 330.00, 10);
products.sql

En la salida de los teses se muestran la traducción de los objetos specification a las condiciones de las consultas.

1
2
3
4
5
6
7
8
9
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=10 and product0_.price=80.0 and product0_.name=?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.date<?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=80.0 or product0_.name=?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=50 or product0_.price=0.0 or product0_.name=?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units>25
Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units<>50 or product0_.price<>80.0 or product0_.name<>?
System.out

Otra de las funcionalidades proporcionadas por Spring Data es hacer consultas basadas en un objeto ejemplo o query by example.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradew test

Referencia:

Comparte el artículo: