Créer un rapport dynamique à l'aide des critères JPA.

Très souvent, dans le développement de l'entreprise, il y a un dialogue:



image



Collision?



Dans cet article, nous verrons comment vous pouvez effectuer des requêtes sur une table avec une liste changeante de critères dans Spring + JPA / Hibernate sans attacher de bibliothèques supplémentaires.



Il n'y a que deux questions principales:



  • Comment assembler dynamiquement une requête SQL
  • Comment passer les conditions pour la formation de cette demande


Pour l'assemblage des requêtes JPA, à partir de 2.0 ( et c'était il y a très, très longtemps ), il propose une solution - Criteria Api, dont les produits sont des objets Specification, nous pouvons en outre passer aux paramètres des méthodes des référentiels JPA.



Spécification - contraintes de requête totales, contient des objets Predicate en tant que conditions WHERE, HAVING. Les prédicats sont des expressions finales qui peuvent être vraies ou fausses.



Une seule condition se compose d'un champ, d'un opérateur de comparaison et d'une valeur à comparer. Les conditions peuvent également être imbriquées. Décrivons complètement la condition avec la classe SearchCriteria:



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


Décrivons maintenant le constructeur lui-même. Il pourra construire une spécification basée sur la liste de conditions soumise, ainsi que combiner plusieurs spécifications d'une certaine manière:



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


Afin de ne pas clôturer un énorme si pour les opérations de comparaison, nous implémentons des opérateurs Map de la forme <Operation, Operator>. L'opérateur doit être capable de construire un seul prédicat. Je vais donner un exemple de l'opération ">", le reste est écrit par analogie:



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


Il reste maintenant à implémenter l'analyse récursive de notre structure SearchCriteria. Notez que la méthode buildPath, qui par Root - la portée de l'objet T trouvera le chemin vers le champ référencé par 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]);
        }
    }


Écrivons un cas de test pour notre constructeur:



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


Au total, nous avons appris à notre application à analyser une expression booléenne à l'aide de Criteria.API. L'ensemble des opérations dans l'implémentation actuelle est limité, mais le lecteur peut implémenter indépendamment celles dont il a besoin. En pratique, la solution est appliquée, mais les utilisateurs ne sont pas intéressés ( ils ont des pattes ) pour construire une expression plus profonde que le premier niveau de récursivité.



Le gestionnaire DISCLAIMER ne prétend pas être complètement universel; si vous devez ajouter des JOINs compliqués, vous devrez entrer dans l'implémentation.



Vous pouvez trouver la version implémentée avec des tests étendus dans mon référentiel sur Github . Vous



pouvez en savoir plus sur Criteria.Api ici .



All Articles