Membuat laporan dinamis menggunakan JPA Criteria.Api

Sangat sering dalam pengembangan perusahaan ada dialog:



gambar



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 ( dan ini sangat, sangat lama sekali ), ia menawarkan solusi - Api Kriteria, yang produknya adalah objek Spesifikasi, kami kemudian dapat meneruskannya ke parameter metode repositori JPA.



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 ( mereka memiliki cakar ) untuk membangun ekspresi yang lebih dalam daripada tingkat rekursi pertama. Penangan



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 .



All Articles