1、概览 {#1概览}
在接下来的一系列文章中,我将带你了解一种用于 REST API 的简单查询语言。
为什么要使用查询语言(Query Language)?因为对于任何足够复杂的 API 来说,仅仅通过简单的字段来搜索/过滤资源是远远不够的。查询语言更加灵活,可以准确过滤选出所需的资源。
2、User 实体 {#2user-实体}
首先,创建一个 User 实体,用于在过滤/搜索 API 中使用:
@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、使用 CriteriaBuilder 过滤 {#3使用-criteriabuilder-过滤}
构建查询抽象是一个平衡问题。一方面,需要足够的灵活性,另一方面,需要保持可控的复杂性。高级查询的功能很简单 - 输入一些约束条件,然后得到一些结果。
来看看如何使用:
@Repository public class UserDAO implements IUserDAO {
@PersistenceContext private EntityManager entityManager;
@Override public List<User> searchUser(List<SearchCriteria> params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<User> query = builder.createQuery(User.class); Root r = query.from(User.class);
Predicate predicate = builder.conjunction(); UserSearchQueryCriteriaConsumer searchConsumer = new UserSearchQueryCriteriaConsumer(predicate, builder, r); params.stream().forEach(searchConsumer); predicate = searchConsumer.getPredicate(); query.where(predicate); List&lt;User&gt; result = entityManager.createQuery(query).getResultList(); return result;
}
@Override public void save(User entity) { entityManager.persist(entity); }
}
UserSearchQueryCriteriaConsumer
类如下:
public class UserSearchQueryCriteriaConsumer implements Consumer<SearchCriteria>{
private Predicate predicate; private CriteriaBuilder builder; private Root r;
@Override public void accept(SearchCriteria param) { if (param.getOperation().equalsIgnoreCase(">")) { predicate = builder.and(predicate, builder .greaterThanOrEqualTo(r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase("<")) { predicate = builder.and(predicate, builder.lessThanOrEqualTo( r.get(param.getKey()), param.getValue().toString())); } else if (param.getOperation().equalsIgnoreCase(":")) { if (r.get(param.getKey()).getJavaType() == String.class) { predicate = builder.and(predicate, builder.like( r.get(param.getKey()), "%" + param.getValue() + "%")); } else { predicate = builder.and(predicate, builder.equal( r.get(param.getKey()), param.getValue())); } } }
// 标准的构造函数、Get、Set 方法
}
如你所见,searchUser
API 接收一系列非常简单的约束条件,根据这些约束条件创建查询,进行搜索并返回结果。
约束类也很简单:
public class SearchCriteria {
private String key;
private String operation;
private Object value;
}
SearchCriteria
实现保存了查询参数(Query Parameter):
key
:用于保存字段名称,例如:firstName
、age
... 等。operation
:用于保存操作 - 例如: 等于、小于 ... 等。value
:用于保存字段值,例如:john
、25
、... 等。
4、测试条件搜索 {#4测试条件搜索}
首先,初始化数据库,添加两个用户进行测试,如下例所示:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { PersistenceConfig.class }) @Transactional @TransactionConfiguration public class JPACriteriaQueryTest {
@Autowired private IUserDAO userApi;
private User userJohn;
private User userTom;
@Before public void init() { userJohn = new User(); userJohn.setFirstName("John"); userJohn.setLastName("Doe"); userJohn.setEmail("john@doe.com"); userJohn.setAge(22); userApi.save(userJohn);
userTom = new User(); userTom.setFirstName(&quot;Tom&quot;); userTom.setLastName(&quot;Doe&quot;); userTom.setEmail(&quot;tom@doe.com&quot;); userTom.setAge(26); userApi.save(userTom);
}
}
尝试检索一个具有指定 firstName
和 lastName
的 User
:
@Test public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "John")); params.add(new SearchCriteria("lastName", ":", "Doe"));
List<User> results = userApi.searchUser(params);
assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));
}
检索具有相同 lastName
的用户列表:
@Test public void givenLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe"));
List<User> results = userApi.searchUser(params); assertThat(userJohn, isIn(results)); assertThat(userTom, isIn(results));
}
检索 age
大于或等于 25 岁的用户:
@Test public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("lastName", ":", "Doe")); params.add(new SearchCriteria("age", ">", "25"));
List<User> results = userApi.searchUser(params);
assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results)));
}
检索实际上并不存在的用户:
@Test public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "Adam")); params.add(new SearchCriteria("lastName", ":", "Fox"));
List<User> results = userApi.searchUser(params); assertThat(userJohn, not(isIn(results))); assertThat(userTom, not(isIn(results)));
}
根据 firstName
进行模糊搜索:
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); params.add(new SearchCriteria("firstName", ":", "jo"));
List<User> results = userApi.searchUser(params);
assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results)));
}
6、UserController {#6usercontroller}
最后,在 REST API 整合这种灵活的持久层检索支持。
创建一个简单的 UserController
,其中的 findAll()
端点使用 search
参数传递整个搜索/过滤表达式:
@Controller public class UserController {
@Autowired private IUserDao api;
@RequestMapping(method = RequestMethod.GET, value = "/users") @ResponseBody public List<User> findAll(@RequestParam(value = "search", required = false) String search) { List<SearchCriteria> params = new ArrayList<SearchCriteria>(); if (search != null) { Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),"); Matcher matcher = pattern.matcher(search + ","); while (matcher.find()) { params.add(new SearchCriteria(matcher.group(1), matcher.group(2), matcher.group(3))); } } return api.searchUser(params); }
}
注意看上述代码是如何通过搜索表达式创建搜索条件对象的。
现在,请求该 API 接口:
http://localhost:8080/users?search=lastName:doe,age>25
响应符合预期,如下:
[{
"id":2,
"firstName":"tom",
"lastName":"doe",
"email":"tom@doe.com",
"age":26
}]
7、总结 {#7总结}
这个简单而强大的实现可以在 REST API 上实现相当多的智能过滤功能。但是,它的边缘还很粗糙,可以改进(下一篇文章)。
Ref:https://www.baeldung.com/rest-search-language-spring-jpa-criteria