JPACriteria.Apiを使用して動的レポートを作成する

非常に頻繁に企業開発で対話があります:



画像



衝突?



この記事では、追加のライブラリをアタッチせずに、Spring + JPA / Hibernateで基準のリストを変更してテーブルに対してクエリを実行する方法について説明します。



主な質問は2つだけです。



  • SQLクエリを動的にアセンブルする方法
  • このリクエストの形成のための条件を渡す方法


2.0から始まるJPAリクエストをアセンブルするために(これは非常に非常に昔のことです)、ソリューションを提供します-製品が仕様オブジェクトであるCriteria Apiは、JPAリポジトリメソッドのパラメータに渡すことができます。



仕様-合計クエリ制約。WHERE、HAVING条件として述語オブジェクトが含まれます。述語は、真または偽の最終式です。



単一の条件は、フィールド、比較演算子、および比較する値で構成されます。条件をネストすることもできます。SearchCriteriaクラスで条件を完全に説明しましょう。



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


それでは、ビルダー自体について説明しましょう。彼は、提出された条件のリストに基づいて仕様を作成し、特定の方法でいくつかの仕様を組み合わせることができます。



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


比較操作のために巨大なifを囲わないようにするために、<Operation、Operator>の形式のMap演算子を実装します。オペレーターは、単一の述語を作成できる必要があります。操作の例を示します ">"、残りは類推によって書かれています:



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


現在、SearchCriteria構造の再帰的解析を実装する必要があります。buildPathメソッドに注意してください。これは、ルート(オブジェクトTのスコープ)によって、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]);
        }
    }


ビルダーのテストケースを書いてみましょう。



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


全体として、Criteria.APIを使用してブール式を解析するようにアプリケーションに教えました。現在の実装での一連の操作は制限されていますが、読者は必要な操作を個別に実装できます。実際には、ソリューションが適用されていますが、ユーザーは、最初のレベルの再帰よりも深い式を作成することに興味がありません(足があります)。



DISCLAIMERハンドラーは、完全にユニバーサルであるとは主張していません。複雑なJOINを追加する必要がある場合は、実装に入る必要があります。



拡張テストを使用し実装されたバージョンは、Githubの私のリポジトリにあります。Criteria.Apiの



詳細については、こちらをご覧ください



All Articles