<div>
<a class="article-series-header" href="javascript:void(0);">該文章是系列的一部分</a>
<div>
<div>
• 使用 Spring 和 JPA Criteria 構建 REST 查詢語言
<br>
• 使用 Spring Data JPA Specifications 構建 REST 查詢語言
<br>
• 使用 Spring Data JPA 和 Querydsl 構建 REST 查詢語言
<br>
• REST 查詢語言 – 高級搜索操作
<br>
<div>
• REST 查詢語言 – 實現 OR 操作 (當前文章)
</div>
• 使用 RSQL 構建 REST 查詢語言
<br>
• 使用 Querydsl Web 支持構建 REST 查詢語言
<br>
</div>
<!-- end of article series inner -->
</div>
<!-- .article-series-links -->
</div>
<!-- end of article series section -->
1. 概述
在本文中,我們將擴展我們先前文章中實現的複雜搜索操作,並將 基於 OR 的搜索條件納入我們的 REST API 查詢語言
2. 實現方法
之前,所有在 <em>搜索</em> 查詢參數中的條件都只通過 AND 運算符進行分組。我們應該改變這一點。
我們應該能夠通過簡單的快速修改現有方法或從頭開始實現這個功能。
採用簡單方法時,我們將標記條件以指示必須使用 OR 運算符進行組合。
例如,以下是用於測試“firstName OR lastName”的API URL:
http://localhost:8080/users?search=firstName:john,'lastName:doe請注意,我們已將條件 <em lastName> 用單引號標記,以進行區分。我們將捕獲此謂詞作為 OR 運算符的條件值對象——<em 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,以考慮在構建Specification<User>時,對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;
}4. UserController 改進
最後,讓我們在我們的控制器中設置一個新的 REST 端點,以便使用 OR 運算符與此搜索功能一起使用。改進的解析邏輯提取了有助於識別包含 OR 運算符的條件的特殊標誌:
@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. 實時測試中使用 OR 條件
在本次實時測試示例中,藉助新的 API 端點,我們將搜索所有姓“john”或姓“doe”的用户。請注意,參數 lastName 中包含單引號,這將其視為“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()));
}6. 使用 OR 條件進行持久性測試
現在,讓我們執行與之前相同的一項測試,該測試針對用户,條件為“姓名為 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. 替代方案
在替代方案中,我們可以將搜索查詢更像一個完整的 SQL 查詢的 WHERE 子句。
例如,以下是使用 firstName 和 age 進行更復雜搜索的 URL:
http://localhost:8082/spring-rest-query-language/auth/users?search=( firstName:john OR firstName:tom ) AND age>22請注意,我們已將單個條件、運算符和分組圓括號之間用空格分隔,以形成有效的前綴表達式。
讓我們使用 CriteriaParser 解析前綴表達式。我們的 CriteriaParser 將給定的前綴表達式分解為標記(條件、括號、AND 和 OR 運算符),併為之創建後綴表達式。
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 中添加一個新方法,從後綴表達式構建搜索 Specification:
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();最後,讓我們在我們的 UserController 中添加另一個 REST 端點,使用新的 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 查詢語言,使其具備使用 OR 運算符進行搜索的功能。