衝突?
この記事では、追加のライブラリをアタッチせずに、Spring + JPA / Hibernateで基準のリストを変更してテーブルに対してクエリを実行する方法について説明します。
主な質問は2つだけです。
- SQLクエリを動的にアセンブルする方法
- このリクエストの形成のための条件を渡す方法
2.0から始まる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の
詳細については、こちらをご覧ください。