Bertabrakan?
Pada artikel ini, kita akan melihat bagaimana Anda dapat membuat kueri pada tabel dengan daftar kriteria yang berubah dalam kerangka Spring + JPA / Hibernate tanpa memasang pustaka tambahan.
Hanya ada dua pertanyaan utama:
- Cara merakit kueri SQL secara dinamis
- Bagaimana melewati kondisi untuk pembentukan permintaan ini
Untuk merakit permintaan JPA, mulai dari 2.0 (
Spesifikasi - batasan kueri total, berisi objek Predikat sebagai WHERE, kondisi HAVING. Predikat adalah ekspresi akhir yang bisa benar atau salah.
Kondisi tunggal terdiri dari bidang, operator perbandingan, dan nilai untuk dibandingkan. Kondisi juga bisa disarangkan. Mari kita gambarkan sepenuhnya kondisi dengan kelas SearchCriteria:
public class SearchCriteria{
//
String key;
// (, .)
SearchOperator operator;
//
String value;
//
private JoinType joinType;
//
private List<SearchCriteria> criteria;
}
Sekarang mari kita gambarkan pembangun itu sendiri. Ia akan dapat membuat spesifikasi berdasarkan daftar kondisi yang diajukan, serta menggabungkan beberapa spesifikasi dengan cara tertentu:
/**
*
*/
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]));
}
};
}
}
Agar tidak menjadi pagar yang besar jika untuk operasi perbandingan, kami menerapkan operator Peta dengan bentuk <Operasi, Operator>. Operator harus dapat membangun satu predikat. Saya akan memberikan contoh operasi ">", sisanya ditulis dengan analogi:
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());
}
}
}
Sekarang tinggal mengimplementasikan penguraian rekursif dari struktur SearchCriteria kami. Perhatikan bahwa metode buildPath, yang oleh Root - cakupan objek T akan menemukan jalur ke bidang yang dirujuk oleh 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]);
}
}
Mari kita tulis kasus uji untuk pembangun kita:
// 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());
}
}
Secara total, kami telah mengajarkan aplikasi kami untuk mengurai ekspresi boolean menggunakan Criteria.API. Himpunan operasi dalam implementasi saat ini terbatas, tetapi pembaca dapat secara mandiri mengimplementasikan yang dia butuhkan. Dalam praktiknya, solusi telah diterapkan, tetapi pengguna tidak tertarik (
DISCLAIMER tidak mengklaim sepenuhnya universal; jika Anda perlu menambahkan JOIN yang rumit, Anda harus masuk ke dalam penerapan.
Anda dapat menemukan versi yang diterapkan dengan pengujian yang diperluas dalam repositori saya di Github . Anda
dapat membaca lebih lanjut tentang Criteria.Api di sini .