Перейти к основному содержимому

Язык запросов REST — реализация операции ИЛИ

· 5 мин. чтения

1. Обзор

В этой быстрой статье мы расширим расширенные операции поиска, которые мы реализовали в предыдущей статье , и включим критерии поиска на основе ИЛИ в наш язык запросов REST API .

2. Подход к реализации

Раньше все критерии в параметре поискового запроса формировали предикаты, сгруппированные только оператором И. Давайте изменим это.

Мы должны иметь возможность реализовать эту функцию либо как простое и быстрое изменение существующего подхода, либо как новый подход с нуля.

В простом подходе мы пометим критерии, чтобы указать, что они должны быть объединены с помощью оператора ИЛИ.

Например, вот URL-адрес для тестирования API для « firstName OR lastName»:

http://localhost:8080/users?search=firstName:john,'lastName:doe

Обратите внимание, что мы пометили фамилию критерия одной кавычкой , чтобы различать его. Мы зафиксируем этот предикат для оператора ИЛИ в нашем объекте значения критерия — SpecSearchCriteria:

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;
}

3. Улучшение UserSpecificationBuilder

Теперь давайте изменим наш построитель спецификаций UserSpecificationBuilder, чтобы он учитывал квалифицированные критерии ИЛИ при построении Specification<User> :

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;
}

4. Улучшение пользовательского контроллера

Наконец, давайте настроим новую конечную точку REST в нашем контроллере, чтобы использовать эту функцию поиска с оператором ИЛИ. Улучшенная логика синтаксического анализа извлекает специальный флаг, который помогает идентифицировать критерии с помощью оператора ИЛИ:

@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();
}

5. Живой тест с условием ИЛИ

В этом живом тестовом примере с новой конечной точкой API мы будем искать пользователей по имени «john» ИЛИ по фамилии «doe». Обратите внимание, что параметр lastName заключен в одинарную кавычку, что определяет его как «предикат ИЛИ»:

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()));
}

6. Тест на устойчивость с условием ИЛИ

Теперь давайте проведем тот же тест, что и выше, на уровне сохраняемости для пользователей с именем «john» ИЛИ фамилией «doe» :

@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));
}

7. Альтернативный подход

В альтернативном подходе мы могли бы предоставить поисковый запрос, больше похожий на полное предложение WHERE SQL-запроса.

Например, вот URL для более сложного поиска по имени и возрасту:

http://localhost:8080/users?search=( firstName:john OR firstName:tom ) AND age>22

Обратите внимание, что мы разделили отдельные критерии, операторы и группирующие скобки пробелом, чтобы сформировать допустимое инфиксное выражение.

Давайте проанализируем инфиксное выражение с помощью CriteriaParser . Наш CriteriaParser разбивает данное инфиксное выражение на токены (критерии, круглые скобки, операторы И и ИЛИ) и создает для них постфиксное выражение:

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;
}

Давайте добавим новый метод в наш построитель спецификаций, GenericSpecificationBuilder, для создания Спецификации поиска из постфиксного выражения:

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();

Наконец, давайте добавим еще одну конечную точку REST в наш UserController для анализа сложного выражения с помощью нового CriteriaParser :

@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);
}

8. Заключение

В этом руководстве мы улучшили наш язык запросов REST, добавив возможность поиска с помощью оператора ИЛИ.

Полную реализацию этой статьи можно найти в проекте GitHub . Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.