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

最后修改: 2015年 1月 29日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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

2.用户实体

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

首先,让我们从一个简单的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;
    
    // standard getters and setters
}

3. Filter Using Specification

3.使用规格过滤

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:

我们将创建一个实现Specification接口的UserSpecification,并且我们将传入我们自己的约束条件来构建实际的查询

public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

    @Override
    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) {
                return builder.like(
                  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:

正如我们所看到的,我们根据一些简单的约束条件创建了一个Specification,我们在下面的SearchCriteria类中表示。

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

4.用户存储库

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

接下来,让我们看一下UserRepository

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

我们只是简单地扩展了JpaSpecificationExecutor,以获得新的规范API。

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

5. Test the Search Queries

5.测试搜索查询

Now let’s test out the new search API.

现在我们来测试一下新的搜索API。

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

首先,让我们创建几个用户,让他们在测试运行时做好准备。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceJPAConfig.class })
@Transactional
@TransactionConfiguration
public class JPASpecificationIntegrationTest {

    @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("john@doe.com");
        userJohn.setAge(22);
        repository.save(userJohn);

        userTom = new User();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        repository.save(userTom);
    }
}

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

接下来,让我们看看如何用给定的姓氏找到用户。

@Test
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:

现在我们要找到一个有给定名和姓的用户

@Test
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.

注:我们使用whereand组合规范。

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

接下来,让我们找到一个具有给定姓氏和最小年龄的用户。

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

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

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

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

现在我们来看看如何搜索一个用户实际上并不存在

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

    List<User> results = 
      repository.findAll(Specification.where(spec1).and(spec2));

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

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

最后,我们将找到一个User只给出部分名字的

@Test
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

6.结合规格

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.

我们将实现一个构建器–UserSpecificationsBuilder–来轻松而流畅地组合Specifications。

But first, let’s see the SpecSearchCriteria object:

但首先,让我们看看SpecSearchCriteria对象。

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 = params.stream()
          .map(UserSpecification::new)
          .collect(Collectors.toList());
        
        Specification result = specs.get(0);

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

7. UserController

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

@Controller
public class UserController {

    @Autowired
    private UserRepository repo;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    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()) {
            builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
        }
        
        Specification<User> spec = builder.build();
        return repo.findAll(spec);
    }
}

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

注意,为了支持其他非英语系统,可以改变Pattern对象。

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

Here is a test URL to test out the API:

这里有一个测试的URL,可以测试一下API。

http://localhost:8080/users?search=lastName:doe,age>25

And here’s the response:

而这里的回应是。

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"tom@doe.com",
    "age":26
}]

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

8.结论

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

这篇文章涵盖了一个简单的实现,可以作为一个强大的REST查询语言的基础。

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.

本文的完整实现可以在GitHub项目中找到。这是一个基于Maven的项目,所以应该很容易导入并按原样运行。

Next »

REST Query Language with Spring Data JPA and Querydsl

« Previous

REST Query Language with Spring and JPA Criteria