2. 概述
本文将扩展之前文章中实现的高级搜索功能,为我们的REST API查询语言新增基于OR的搜索条件。这个改进能让API支持更灵活的查询组合,满足实际业务中常见的"或"条件查询需求。
3. 实现方案
之前的实现中,所有搜索条件都只能通过AND运算符组合。现在我们来改变这个限制。
实现方案有两种选择:
- ✅ 快速改造:在现有方案基础上添加OR标记
- ❌ 重新设计:从零开始构建新方案
我们采用更简单直接的第一种方案:通过特殊标记标识需要OR组合的条件。例如查询"firstName OR lastName"的URL如下:
http://localhost:8080/users?search=firstName:john,'lastName:doe
注意我们用单引号标记了lastName
条件。这个标记会在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;
}
4. UserSpecificationBuilder 改进
现在修改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;
}
这个改进让构建器能智能处理OR标记条件,避免传统实现中常见的逻辑混乱问题。
5. UserController 改进
最后在控制器中新增REST接口,实现带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();
}
6. 带OR条件的实时测试
用新接口测试"firstName为john OR lastName为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()));
}
7. 带OR条件的持久层测试
在持久层执行相同测试,查询"firstName为john OR lastName为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));
}
8. 替代方案
另一种方案是让搜索查询更像完整的SQL WHERE子句。例如更复杂的firstName
和age
查询:
http://localhost:8082/spring-rest-query-language/auth/users?search=( firstName:john OR firstName:tom ) AND age>22
这里用空格分隔条件、运算符和括号,形成有效的中缀表达式。通过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();
最后在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);
}
9. 总结
本文为REST查询语言增加了OR操作支持,提供了两种实现方案:
- 简单标记方案:快速实现,适合简单场景
- 完整表达式方案:支持复杂查询,适合高级需求
完整实现代码可在GitHub项目获取。这是一个Maven项目,可以直接导入运行。
⚠️ 实际项目中建议根据复杂度选择方案:简单查询用标记方案,复杂业务用表达式方案,避免过度设计。