這篇文章是系列的一部分:
• 使用 Spring 和 JPA Criteria 的 REST 查詢語言
• 使用 Spring Data JPA Specifications 的 REST 查詢語言
• 使用 Spring Data JPA 和 Querydsl 的 REST 查詢語言
• REST 查詢語言 – 高級搜索操作
• REST 查詢語言 – 實現 OR 操作
• 使用 RSQL 的 REST 查詢語言 (當前文章)
• REST 查詢語言 – Querydsl Web 支持
1. 概述
在本文檔系列中的第五篇文章中,我們將通過使用
rsql-parser 庫來演示如何構建 REST API 查詢語言。
RSQL 是 Feed Item 查詢語言(FIQL)的超集——一種簡潔易用的過濾語法,非常適合用於 Feed;因此,它自然地融入到 REST API 中。
2. 準備工作
首先,我們需要添加一個 Maven
依賴項到庫中:
<dependency>
<groupId>cz.jirutka.rsql</groupId>
<artifactId>rsql-parser</artifactId>
<version>2.1.0</version>
</dependency>
並且定義我們將在示例中使用的主要實體 – User:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
private int age;
}
3. Parse the Request
通過 RSQL 表達式在內部表示的方式是節點,並且使用訪問者模式來解析輸入。
考慮到這一點,我們將實現 RSQLVisitor 接口,並創建我們的訪問者實現——CustomRsqlVisitor:
public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {
private GenericRsqlSpecBuilder<T> builder;
public CustomRsqlVisitor() {
builder = new GenericRsqlSpecBuilder<T>();
}
@Override
public Specification<T> visit(AndNode node, Void param) {
return builder.createSpecification(node);
}
@Override
public Specification<T> visit(OrNode node, Void param) {
return builder.createSpecification(node);
}
@Override
public Specification<T> visit(ComparisonNode node, Void params) {
return builder.createSecification(node);
}
}
現在我們需要處理持久化並構造從每個訪問的節點中構建查詢。
我們將使用之前使用的 Spring Data JPA Specifications——並且我們將實現一個 Specification 構建器來 從每個訪問的節點中構建 Specifications:
public class GenericRsqlSpecBuilder<T> {
public Specification<T> createSpecification(Node node) {
if (node instanceof LogicalNode) {
return createSpecification((LogicalNode) node);
}
if (node instanceof ComparisonNode) {
return createSpecification((ComparisonNode) node);
}
return null;
}
public Specification<T> createSpecification(LogicalNode logicalNode) {
List<Specification> specs = logicalNode.getChildren()
.stream()
.map(node -> createSpecification(node))
.filter(Objects::nonNull)
.collect(Collectors.toList());
Specification<T> result = specs.get(0);
if (logicalNode.getOperator() == LogicalOperator.AND) {
for (int i = 1; i < specs.size(); i++) {
result = Specification.where(result).and(specs.get(i));
}
} else if (logicalNode.getOperator() == LogicalOperator.OR) {
for (int i = 1; i < specs.size(); i++) {
result = Specification.where(result).or(specs.get(i));
}
}
return result;
}
public Specification<T> createSpecification(ComparisonNode comparisonNode) {
Specification<T> result = Specification.where(
new GenericRsqlSpecification<T>(
comparisonNode.getSelector(),
comparisonNode.getOperator(),
comparisonNode.getArguments()
)
);
return result;
}
}
注意以下內容:
- LogicalNode 是 AND / OR Node,並且具有多個子節點
- ComparisonNode 沒有子節點,並且它持有 Selector, Operator 和 Arguments
例如,對於查詢 “name==john”——我們有:
- Selector: “name”
- Operator: “==”
- Arguments:[john]
4. 創建自定義 規範
在構建查詢時,我們使用了 規範 :
public class GenericRsqlSpecification<T> implements Specification<T> {
private String property;
private ComparisonOperator operator;
private List<String> arguments;
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Object> args = castArguments(root);
Object argument = args.get(0);
switch (RsqlSearchOperation.getSimpleOperator(operator)) {
case EQUAL: {
if (argument instanceof String) {
return builder.like(root.get(property), argument.toString().replace('*', '%'));
} else if (argument == null) {
return builder.isNull(root.get(property));
} else {
return builder.equal(root.get(property), argument);
}
}
case NOT_EQUAL: {
if (argument instanceof String) {
return builder.notLike(root.<String> get(property), argument.toString().replace('*', '%'));
} else if (argument == null) {
return builder.isNotNull(root.get(property));
} else {
return builder.notEqual(root.<String> get(property), argument);
}
}
case GREATER_THAN: {
return builder.greaterThan(root.<String> get(property), argument.toString());
}
case GREATER_THAN_OR_EQUAL: {
return builder.greaterThanOrEqualTo(root.<String> get(property), argument.toString());
}
case LESS_THAN: {
return builder.lessThan(root.<String> get(property), argument.toString());
}
case LESS_THAN_OR_EQUAL: {
return builder.lessThanOrEqualTo(root.<String> get(property), argument.toString());
}
case IN:
return root.get(property).in(args);
case NOT_IN:
return builder.not(root.get(property).in(args));
}
return null;
}
private List<Object> castArguments(final Root<T> root) {
Class<? extends Object> type = root.get(property).getJavaType();
List<Object> args = arguments.stream().map(arg -> {
if (type.equals(Integer.class)) {
return Integer.parseInt(arg);
} else if (type.equals(Long.class)) {
return Long.parseLong(arg);
} else {
return arg;
}
}).collect(Collectors.toList());
return args;
}
// standard constructor, getter, setter
}
注意,規範使用了泛型,並且與任何特定的實體(如 User)沒有關聯。
接下來是我們的 枚舉 “RsqlSearchOperation ” ,其中包含默認的 rsql-parser 運算符:
public enum RsqlSearchOperation {
EQUAL(RSQLOperators.EQUAL),
NOT_EQUAL(RSQLOperators.NOT_EQUAL),
GREATER_THAN(RSQLOperators.GREATER_THAN),
GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL),
LESS_THAN(RSQLOperators.LESS_THAN),
LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL),
IN(RSQLOperators.IN),
NOT_IN(RSQLOperators.NOT_IN);
private ComparisonOperator operator;
private RsqlSearchOperation(ComparisonOperator operator) {
this.operator = operator;
}
public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
for (RsqlSearchOperation operation : values()) {
if (operation.getOperator() == operator) {
return operation;
}
}
return null;
}
}
5. Test Search Queries
Let’s now start testing our new and flexible operations through some real-world scenarios:
First – let’s initialize the data:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class RsqlTest {
@Autowired
private UserRepository repository;
private User userJohn;
private User userTom;
@Before
public void init() {
userJohn = new User();
userJohn.setFirstName("john");
userJohn.setLastName("doe");
userJohn.setEmail("[email protected]");
userJohn.setAge(22);
repository.save(userJohn);
userTom = new User();
userTom.setFirstName("tom");
userTom.setLastName("doe");
userTom.setEmail("[email protected]");
userTom.setAge(26);
repository.save(userTom);
}
}
Now let’s test the different operations:
5.1. Test Equality
In the following example – we’ll search for users by their
first and
last name:
@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe");
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
List<User> results = repository.findAll(spec);
assertThat(userJohn, isIn(results));
assertThat(userTom, not(isIn(results)));
}
5.2. Test Negation
Next, let’s search for users that by the their
first name not “john”:
@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
Node rootNode = new RSQLParser().parse("firstName!=john");
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
List<User> results = repository.findAll(spec);
assertThat(userTom, isIn(results));
assertThat(userJohn, not(isIn(results)));
}
5.3. Test Greater Than
Next – we will search for users with
age greater than “
25”:
@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
Node rootNode = new RSQLParser().parse("age>25");
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
List<User> results = repository.findAll(spec);
assertThat(userTom, isIn(results));
assertThat(userJohn, not(isIn(results)));
}
5.4. Test Like
Next – we will search for users with their
first name starting with “
jo”:
@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
Node rootNode = new RSQLParser().parse("firstName==jo*");
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
List<User> results = repository.findAll(spec);
assertThat(userJohn, isIn(results));
assertThat(userTom, not(isIn(results)));
}
5.5. Test IN
Next – we will search for users their
first name is “
john” or “
jack“:
@Test
public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() {
Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)");
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
List<User> results = repository.findAll(spec);
assertThat(userJohn, isIn(results));
assertThat(userTom, not(isIn(results)));
}
6. UserController
最後,讓我們將所有內容與控制器聯繫起來:
@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllByRsql(@RequestParam(value = "search") String search) {
Node rootNode = new RSQLParser().parse(search);
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
return dao.findAll(spec);
}
以下是一個示例 URL:
http://localhost:8082/spring-rest-query-language/auth/users?search=firstName==jo*;age<25
響應:
[{
"id":1,
"firstName":"john",
"lastName":"doe",
"email":"[email protected]",
"age":24
}]
7. 結論
本教程闡述瞭如何構建 REST API 的 Query/搜索語言,無需重新發明語法,而是使用 FIQL / RSQL。
下一條 →
REST Query Language with Querydsl Web Support
< 上一篇
REST Query Language – Implementing OR Operation
0 位用戶收藏了這個故事!