Fazendo um relatório dinâmico usando JPA Criteria.Api

Muitas vezes, no desenvolvimento corporativo, há um diálogo:



imagem



Colidiu?



Neste artigo, veremos como você pode fazer consultas em uma tabela com uma lista de alteração de critérios na estrutura Spring + JPA / Hibernate sem anexar bibliotecas adicionais.



Existem apenas duas questões principais:



  • Como montar dinamicamente uma consulta SQL
  • Como passar as condições para a formação deste pedido


Para a montagem de solicitações JPA, a partir do 2.0 ( e isso foi há muito, muito tempo ), oferece uma solução - Criteria Api, cujos produtos são objetos de Especificação, podemos então passá-la para os parâmetros dos métodos dos repositórios JPA.



Especificação - restrições de consulta total, contém objetos Predicado como condições WHERE, HAVING. Predicados são expressões finais que podem ser verdadeiras ou falsas.



Uma única condição consiste em um campo, um operador de comparação e um valor a ser comparado. As condições também podem ser aninhadas. Vamos descrever completamente a condição com a classe SearchCriteria:



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


Agora vamos descrever o próprio construtor. Ele será capaz de construir uma especificação com base na lista de condições submetida, bem como combinar várias especificações de uma determinada maneira:



/**
*  
*/
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]));
            }
 
        };
    }
}


Para não bloquear um enorme if para operações de comparação, implementamos operadores de mapa da forma <Operação, Operador>. O operador deve ser capaz de construir um único predicado. Vou dar um exemplo da operação ">", o resto é escrito por analogia:



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());
        }
    }
}


Agora resta implementar a análise recursiva de nossa estrutura SearchCriteria. Observe que o método buildPath, que por Root - o escopo do objeto T encontrará o caminho para o campo referenciado por 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]);
        }
    }


Vamos escrever um caso de teste para nosso construtor:



// 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());
    }
 
}


No total, ensinamos nosso aplicativo a analisar uma expressão booleana usando o Criteria.API. O conjunto de operações na implementação atual é limitado, mas o leitor pode implementar independentemente aquelas de que necessita. Na prática, a solução foi aplicada, mas os usuários não estão interessados ​​( eles têm patas ) em construir uma expressão mais profunda do que o primeiro nível de recursão.



O manipulador DISCLAIMER não alega ser completamente universal; se você precisar adicionar JOINs complicados, terá que entrar na implementação.



Você pode encontrar a versão implementada com testes estendidos em meu repositório no Github.Você



pode ler mais sobre Criteria.Api aqui .



All Articles