در بخش های قبلی دیدیم که یکسری عملیات پیچیده کوئری های Criteria را توانسته بودیم بصورت AND ترکیب کنیم در این بخش به پیاده سازی عمل OR و ترکیب آن با سایر عملیات میپردازیم
ابتدا برای مشخص کردن عملیات درخواستی OR از سمت کلاینت نیاز است کلاسی برای آن ایجاد کنیم :
public SpecSearchCriteria( String orPredicate, String key, SearchOperation operation, Object value) { super(); this.orPredicate = orPredicate != null && orPredicate.equals(SearchOperation.OR_PREDICATE_FLAG); this.key = key; this.operation = operation; this.value = value; }
کوئری میتواند از سمت کلاینت شکلی مشابه این حالت داشته باشد :
http://localhost:8080/users?search=firstName:john,'lastName:doe
در اینجا عمل or با یک ' تک کوتیشن مشخص شده است
حال تغییراتی در کلاس UserSpecificationBuilder میدهیم که بتواند عمل or را تشخیص دهد :
public Specification<User> build() { if (params.size() == 0) { return null; } Specification<User> result = new UserSpecification(params.get(0)); for (int i = 1; i < params.size(); i++) { result = params.get(i).isOrPredicate() ? Specification.where(result).or(new UserSpecification(params.get(i))) : Specification.where(result).and(new UserSpecification(params.get(i))); } return result; }
ساخت کلاس کنترلری که به درخواست های فیلتر/سرچ رسیدگی میکند :
@GetMapping("/users/espec") @ResponseBody public List<User> findAllByOrPredicate(@RequestParam String search) { Specification<User> spec = resolveSpecification(search); return dao.findAll(spec); } protected Specification<User> resolveSpecification(String searchParameters) { UserSpecificationsBuilder builder = new UserSpecificationsBuilder(); String operationSetExper = Joiner.on("|") .join(SearchOperation.SIMPLE_OPERATION_SET); Pattern pattern = Pattern.compile( "(\\p{Punct}?)(\\w+?)(" + operationSetExper + ")(\\p{Punct}?)(\\w+?)(\\p{Punct}?),"); Matcher matcher = pattern.matcher(searchParameters + ","); while (matcher.find()) { builder.with(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(5), matcher.group(4), matcher.group(6)); } return builder.build(); }
تست همراه با عمل or :
private String EURL_PREFIX = "http://localhost:8082/spring-rest-full/auth/users/espec?search="; @Test public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() { Response response = givenAuth().get(EURL_PREFIX + "firstName:john,'lastName:doe"); String result = response.body().asString(); assertTrue(result.contains(userJohn.getEmail())); assertTrue(result.contains(userTom.getEmail())); }
دو عمل or ترکیب شده در یک کوئری :
@Test public void givenFirstOrLastName_whenGettingListOfUsers_thenCorrect() { UserSpecificationsBuilder builder = new UserSpecificationsBuilder(); SpecSearchCriteria spec = new SpecSearchCriteria("firstName", SearchOperation.EQUALITY, "john"); SpecSearchCriteria spec1 = new SpecSearchCriteria("'","lastName", SearchOperation.EQUALITY, "doe"); List<User> results = repository .findAll(builder.with(spec).with(spec1).build()); assertThat(results, hasSize(2)); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results)); }
فرض کنید شکل عملیاتی که از کلاینت میخواهیم دریافت کنیم شبیه به SQL باشد مانند حالت زیر :
http://localhost:8080/users?search=( firstName:john OR firstName:tom ) AND age>22
برای تجزیه کردن عملیات ارسالی :
public Deque<?> parse(String searchParam) { Deque<Object> output = new LinkedList<>(); Deque<String> stack = new LinkedList<>(); Arrays.stream(searchParam.split("\\s+")).forEach(token -> { if (ops.containsKey(token)) { while (!stack.isEmpty() && isHigerPrecedenceOperator(token, stack.peek())) { output.push(stack.pop().equalsIgnoreCase(SearchOperation.OR_OPERATOR) ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR); } stack.push(token.equalsIgnoreCase(SearchOperation.OR_OPERATOR) ? SearchOperation.OR_OPERATOR : SearchOperation.AND_OPERATOR); } else if (token.equals(SearchOperation.LEFT_PARANTHESIS)) { stack.push(SearchOperation.LEFT_PARANTHESIS); } else if (token.equals(SearchOperation.RIGHT_PARANTHESIS)) { while (!stack.peek().equals(SearchOperation.LEFT_PARANTHESIS)) { output.push(stack.pop()); } stack.pop(); } else { Matcher matcher = SpecCriteraRegex.matcher(token); while (matcher.find()) { output.push(new SpecSearchCriteria( matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4), matcher.group(5))); } } }); while (!stack.isEmpty()) { output.push(stack.pop()); } return output; }
کلاس Specification Builder ای که قبلا ساخته بودیم هم میتوانیم با ارائه متد زیر بهینه تر کنیم :
public Specification<U> build(Deque<?> postFixedExprStack, Function<SpecSearchCriteria, Specification<U>> converter) { Deque<Specification<U>> specStack = new LinkedList<>(); while (!postFixedExprStack.isEmpty()) { Object mayBeOperand = postFixedExprStack.pollLast(); if (!(mayBeOperand instanceof String)) { specStack.push(converter.apply((SpecSearchCriteria) mayBeOperand)); } else { Specification<U> operand1 = specStack.pop(); Specification<U> operand2 = specStack.pop(); if (mayBeOperand.equals(SearchOperation.AND_OPERATOR)) { specStack.push(Specification.where(operand1) .and(operand2)); } else if (mayBeOperand.equals(SearchOperation.OR_OPERATOR)) { specStack.push(Specification.where(operand1) .or(operand2)); } } } return specStack.pop();
همینطور برای تست این حالت پیچیده تر نیاز است تغییراتی در کنترلر بدهیم که در زیر کنترلر را در آدرس دیگری تعریف میکنیم :
@GetMapping("/users/spec/adv") @ResponseBody public List<User> findAllByAdvPredicate(@RequestParam String search) { Specification<User> spec = resolveSpecificationFromInfixExpr(search); return dao.findAll(spec); } protected Specification<User> resolveSpecificationFromInfixExpr(String searchParameters) { CriteriaParser parser = new CriteriaParser(); GenericSpecificationsBuilder<User> specBuilder = new GenericSpecificationsBuilder<>(); return specBuilder.build(parser.parse(searchParameters), UserSpecification::new); }