Hacer un informe dinámico usando JPA Criteria.Api

Muy a menudo en el desarrollo empresarial hay un diálogo:



imagen



¿Chocó?



En este artículo, veremos cómo puede realizar consultas en una tabla con una lista cambiante de criterios en el marco Spring + JPA / Hibernate sin adjuntar bibliotecas adicionales.



Solo hay dos preguntas principales:



  • Cómo ensamblar dinámicamente una consulta SQL
  • Cómo pasar las condiciones para la formación de esta solicitud


Para ensamblar solicitudes JPA, a partir de 2.0 ( y esto fue hace mucho, mucho tiempo ), ofrece una solución: Criteria Api, cuyos productos son objetos de especificación, luego podemos pasarlo a los parámetros de los métodos de los repositorios JPA.



Especificación: restricciones de consulta totales, contiene objetos predicados como DÓNDE, CON condiciones. Los predicados son expresiones finales que pueden ser verdaderas o falsas.



Una sola condición consta de un campo, un operador de comparación y un valor para comparar. Las condiciones también se pueden anidar. Describamos completamente la condición con la clase SearchCriteria:



public class SearchCriteria{
    // 
    String key;
    // (,   .)
    SearchOperator operator;
    //  
    String value;
    //   
    private JoinType joinType;
    //  
    private List<SearchCriteria> criteria;
}


Ahora describamos al constructor en sí. Podrá construir una especificación basada en la lista de condiciones enviada, así como combinar varias especificaciones de cierta manera:



/**
*  
*/
public class JpaSpecificationsBuilder<T> {

    //  join- 
    private Map<String,Join<Object, Object>> joinMap = new HashMap<>();

    //   
    private Map<SearchOperation, PredicateBuilder> predicateBuilders = Stream.of(
            new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.EQ,new EqPredicateBuilder()),
            new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MORE,new MorePredicateBuilder()),
            new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MOREQ,new MoreqPredicateBuilder()),
            new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESS,new LessPredicateBuilder()),
            new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESSEQ,new LesseqPredicateBuilder())
    ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
 
    /**
     *     
     */
    public Specification<T> buildSpecification(SearchCriteria criterion){
        this.joinMap.clear();
        return (root, query, cb) -> buildPredicate(root,cb,criterion);
    }
     
    /**
    *  
    */
    public Specification<T> mergeSpecifications(List<Specification> specifications, JoinType joinType) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
 
            specifications.forEach(specification -> predicates.add(specification.toPredicate(root, query, cb)));
 
            if(joinType.equals(JoinType.AND)){
                return cb.and(predicates.toArray(new Predicate[0]));
            }
            else{
                return cb.or(predicates.toArray(new Predicate[0]));
            }
 
        };
    }
}


Con el fin de no cerrar una gran cantidad de operaciones de comparación, implementamos operadores de mapa de la forma <Operación, Operador>. El operador debe poder construir un solo predicado. Daré un ejemplo de la operación ">", el resto están escritos por analogía:



public class EqPredicateBuilder implements PredicateBuilder {
    @Override
    public SearchOperation getManagedOperation() {
        return SearchOperation.EQ;
    }
 
    @Override
    public Predicate getPredicate(CriteriaBuilder cb, Path path, SearchCriteria criteria) {
        if(criteria.getValue() == null){
            return cb.isNull(path);
        }
 
        if(LocalDateTime.class.equals(path.getJavaType())){
            return cb.equal(path,LocalDateTime.parse(criteria.getValue()));
        }
        else {
            return cb.equal(path, criteria.getValue());
        }
    }
}


Ahora necesitamos implementar el análisis recursivo de nuestra estructura SearchCriteria. Tenga en cuenta que el método buildPath, que por Root - el alcance del objeto T encontrará la ruta al campo al que hace referencia SearchCriteria.key:



private Predicate buildPredicate(Root<T> root, CriteriaBuilder cb, SearchCriteria criterion) {
    if(criterion.isComplex()){
        List<Predicate> predicates = new ArrayList<>();
        for (SearchCriteria subCriterion : criterion.getCriteria()) {
            //     ,        
            predicates.add(buildPredicate(root,cb,subCriterion));
        }
        if(JoinType.AND.equals(criterion.getJoinType())){
            return cb.and(predicates.toArray(new Predicate[0]));
        }
        else{
            return cb.or(predicates.toArray(new Predicate[0]));
        }
    }
    return predicateBuilders.get(criterion.getOperation()).getPredicate(cb,buildPath(root, criterion.getKey()),criterion);
}
 
private Path buildPath(Root<T> root, String key) {

        if (!key.contains(".")) {
            return root.get(key);
        } else {
            String[] path = key.split("\\.");

            String subPath = path[0];
            if(joinMap.get(subPath) == null){
                joinMap.put(subPath,root.join(subPath));
            }
            for (int i = 1; i < path.length-1; i++) {
                subPath = Stream.of(path).limit(i+1).collect(Collectors.joining("."));
                if(joinMap.get(subPath) == null){
                    String prevPath = Stream.of(path).limit(i).collect(Collectors.joining("."));
                    joinMap.put(subPath,joinMap.get(prevPath).join(path[i]));
                }
            }

            return joinMap.get(subpath).get(path[path.length - 1]);
        }
    }


Escribamos un caso de prueba para nuestro constructor:



// Entity
@Entity
public class ExampleEntity {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    public int value;
 
    public ExampleEntity(int value){
        this.value = value;
    }
 
}
 
...
 
// 
@Repository
public interface ExampleEntityRepository extends JpaRepository<ExampleEntity,Long>, JpaSpecificationExecutor<ExampleEntity> {
}
 
...
 
// 
/*
  
*/
public class JpaSpecificationsTest {
 
    @Autowired
    private ExampleEntityRepository exampleEntityRepository;
 
    @Test
    public void getWhereMoreAndLess(){
        exampleEntityRepository.save(new ExampleEntity(3));
        exampleEntityRepository.save(new ExampleEntity(5));
        exampleEntityRepository.save(new ExampleEntity(0));
 
        SearchCriteria criterion = new SearchCriteria(
                null,null,null,
                Arrays.asList(
                        new SearchCriteria("value",SearchOperation.MORE,"0",null,null),
                        new SearchCriteria("value",SearchOperation.LESS,"5",null,null)
                ),
                JoinType.AND
        );
        assertEquals(1,exampleEntityRepository.findAll(specificationsBuilder.buildSpecification(criterion)).size());
    }
 
}


En total, hemos enseñado a nuestra aplicación a analizar una expresión booleana utilizando Criteria.API. El conjunto de operaciones en la implementación actual es limitado, pero el lector puede implementar independientemente las que necesite. En la práctica, se ha aplicado la solución, pero a los usuarios no les interesa ( tienen patas ) construir una expresión más profunda que el primer nivel de recursividad.



El controlador DISCLAIMER no pretende ser completamente universal; si necesita agregar JOIN complicados, tendrá que ir a la implementación.



Puedes encontrar la versión implementada con pruebas extendidas en mi repositorio en Github . Puedes



leer más sobre Criteria.Api aquí .



All Articles