REST Query Language with Spring Data JPA Specifications – 使用Spring Data JPA规范的REST查询语言

最后修改: 2015年 1月 29日


1. Overview


In this tutorial, we’ll build a Search/Filter REST API using Spring Data JPA and Specifications.

在本教程中,我们将使用Spring Data JPA和规范构建一个搜索/过滤REST API

We started looking at a query language in the first article of this series with a JPA Criteria-based solution.

我们在第一篇文章中开始关注查询语言,该系列文章采用基于JPA Criteria的方案。

So, why a query language? Because searching/filtering our resources by very simple fields just isn’t enough for APIs that are too complex. A query language is more flexible and allows us to filter down to exactly the resources we need.

那么,为什么要使用查询语言?因为通过非常简单的字段搜索/过滤我们的资源对于过于复杂的 API 来说是不够的。查询语言更加灵活,使我们能够准确地过滤到我们需要的资源。

2. User Entity


First, let’s start with a simple User entity for our Search API:


public class User {
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
    // standard getters and setters

3. Filter Using Specification


Now let’s get straight into the most interesting part of the problem: querying with custom Spring Data JPA Specifications.

现在让我们直接进入问题中最有趣的部分:用自定义Spring Data JPA Specifications进行查询。

We’ll create a UserSpecification that implements the Specification interface, and we’re going to pass in our own constraint to construct the actual query:


public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

    public Predicate toPredicate
      (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        if (criteria.getOperation().equalsIgnoreCase(">")) {
            return builder.greaterThanOrEqualTo(
              root.<String> get(criteria.getKey()), criteria.getValue().toString());
        else if (criteria.getOperation().equalsIgnoreCase("<")) {
            return builder.lessThanOrEqualTo(
              root.<String> get(criteria.getKey()), criteria.getValue().toString());
        else if (criteria.getOperation().equalsIgnoreCase(":")) {
            if (root.get(criteria.getKey()).getJavaType() == String.class) {
                  root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
            } else {
                return builder.equal(root.get(criteria.getKey()), criteria.getValue());
        return null;

As we can see, we create a Specification based on some simple constraints that we represent in the following SearchCriteria class:


public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;

The SearchCriteria implementation holds a basic representation of a constraint, and it’s based on this constraint that we’re going construct the query:

SearchCriteria 实现持有一个约束条件的基本表示,而我们正是要基于这个约束条件来构造查询。

  • key: the field name, for example, firstName, age, etc.
  • operation: the operation, for example, equality, less than, etc.
  • value: the field value, for example, john, 25, etc.

Of course, the implementation is simplistic and can be improved. However, it’s a solid base for the powerful and flexible operations we need.


4. The UserRepository


Next, let’s take a look at the UserRepository.


We’re simply extending the JpaSpecificationExecutor to get the new Specification APIs:


public interface UserRepository 
  extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}

5. Test the Search Queries


Now let’s test out the new search API.


First, let’s create a few users to have them ready when the tests run:


@ContextConfiguration(classes = { PersistenceJPAConfig.class })
public class JPASpecificationIntegrationTest {

    private UserRepository repository;

    private User userJohn;
    private User userTom;

    public void init() {
        userJohn = new User();

        userTom = new User();

Next, let’s see how to find users with given last name:


public void givenLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));

Now we’ll find a user with given both first and last name:


public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "john"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));
    List<User> results = repository.findAll(Specification.where(spec1).and(spec2));

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));

Note: We used where and and to combine Specifications.


Next, let’s find a user with given both last name and minimum age:


public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("age", ">", "25"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "doe"));

    List<User> results = 

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));

Now we’ll see how to search for a User that doesn’t actually exist:


public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec1 = 
      new UserSpecification(new SearchCriteria("firstName", ":", "Adam"));
    UserSpecification spec2 = 
      new UserSpecification(new SearchCriteria("lastName", ":", "Fox"));

    List<User> results = 

    assertThat(userJohn, not(isIn(results)));
    assertThat(userTom, not(isIn(results)));  

Finally, we’ll find a User given only part of the first name:


public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = 
      new UserSpecification(new SearchCriteria("firstName", ":", "jo"));
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));

6. Combine Specifications


Next, let’s take a look at combining our custom Specifications to use multiple constraints and filter according to multiple criteria.


We’re going to implement a builder — UserSpecificationsBuilder — to easily and fluently combine Specifications.


But first, let’s see the SpecSearchCriteria object:


public class SpecSearchCriteria {

    private String key;
    private SearchOperation operation;
    private Object value;
    private boolean orPredicate;

    public boolean isOrPredicate() {
        return orPredicate;
public class UserSpecificationsBuilder {
    private final List<SearchCriteria> params;

    public UserSpecificationsBuilder() {
        params = new ArrayList<SearchCriteria>();

    public UserSpecificationsBuilder with(String key, String operation, Object value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;

    public Specification<User> build() {
        if (params.size() == 0) {
            return null;

        List<Specification> specs =
        Specification result = specs.get(0);

        for (int i = 1; i < params.size(); i++) {
            result = params.get(i)
                ? Specification.where(result)
                : Specification.where(result)
        return result;

7. UserController


Finally, let’s use this new persistence search/filter functionality and set up the REST API by creating a UserController with a simple search operation:

最后,让我们使用这个新的持久化搜索/过滤功能,通过创建一个带有简单搜索操作的UserController来设置REST API

public class UserController {

    private UserRepository repo;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    public List<User> search(@RequestParam(value = "search") String search) {
        UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
        Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),");
        Matcher matcher = pattern.matcher(search + ",");
        while (matcher.find()) {
        Specification<User> spec =;
        return repo.findAll(spec);

Note that to support other non-English systems, the Pattern object could be changed:


Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);

Here is a test URL to test out the API:



And here’s the response:



Since the searches are split by a “,” in our Pattern example, the search terms can’t contain this character. The pattern also doesn’t match whitespace.

由于在我们的Pattern例子中,搜索被”, “分割,所以搜索词不能包含这个字符。该模式也不匹配空白处。

If we want to search for values containing commas, we can consider using a different separator such as “;”.


Another option would be to change the pattern to search for values between quotes and then strip these from the search term:


Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\"([^\"]+)\")");

8. Conclusion


This article covered a simple implementation that can be the base of a powerful REST query language.


We’ve made good use of Spring Data Specifications to make sure we keep the API away from the domain and have the option to handle many other types of operations.

我们很好地利用了Spring Data Specifications,以确保我们的API远离领域,并且可以选择处理许多其他类型的操作。

The full implementation of this article can be found in the GitHub project. This is a Maven-based project, so it should be easy to import and run as it is.


Next »

REST Query Language with Spring Data JPA and Querydsl

« Previous

REST Query Language with Spring and JPA Criteria