اصطدمت؟
في هذه المقالة ، سننظر في كيفية إجراء استعلامات على جدول بقائمة متغيرة من المعايير في Spring + JPA / Hibernate دون إرفاق مكتبات إضافية.
هناك سؤالان رئيسيان فقط:
- كيفية تجميع استعلام SQL ديناميكيًا
- كيفية تمرير شروط تشكيل هذا الطلب
لتجميع طلبات JPA ، بدءًا من 2.0 (
المواصفات - إجمالي قيود الاستعلام ، يحتوي على كائنات مسند مثل حيث ، وجود شروط. المسندات هي التعبيرات النهائية التي يمكن أن تكون صحيحة أو خاطئة.
يتكون الشرط الفردي من حقل وعامل مقارنة وقيمة للمقارنة. يمكن أيضًا أن تتداخل الشروط. دعنا نصف الحالة بشكل كامل باستخدام فئة 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]));
}
};
}
}
من أجل عدم وضع سياج ضخم لعمليات المقارنة ، فإننا نطبق مشغلي الخرائط من النموذج <عملية ، مشغل>. يجب أن يكون المشغل قادرًا على إنشاء مسند واحد. سأقدم مثالاً على العملية ">" ، والباقي مكتوب بالقياس:
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. مجموعة العمليات في التنفيذ الحالي محدودة ، لكن يمكن للقارئ تنفيذ ما يحتاج إليه بشكل مستقل. في الممارسة العملية ، تم تطبيق الحل ، لكن المستخدمين غير مهتمين (
إخلاء المسؤولية أنه عالمي تمامًا ؛ إذا كنت بحاجة إلى إضافة JOINs المعقدة ، فسيتعين عليك الدخول في التنفيذ.
يمكنك العثور على الإصدار المطبق مع الاختبارات الموسعة في مستودعي على Github . يمكنك
قراءة المزيد حول Criteria.Api هنا .