REST Query Language with Spring and JPA Criteria – 使用Spring和JPA标准的REST查询语言

最后修改: 2015年 1月 27日

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

1. Overview

1.概述

In this first article of this new series, we’ll explore a simple query language for a REST API. We’ll make good use of Spring for the REST API and JPA 2 Criteria for the persistence aspects.

这个新系列的第一篇文章中,我们将探讨REST API的简单查询语言。我们将在REST API方面充分利用Spring,在持久化方面利用JPA 2 Criteria。

Why a query language? Because – for any complex enough API – searching/filtering your resources by very simple fields is simply not enough. A query language is more flexible, and allows you to filter down to exactly the resources you need.

为什么是查询语言?因为对于任何足够复杂的API来说,通过非常简单的字段来搜索/过滤您的资源是远远不够的。查询语言更加灵活,并允许您精确地过滤到您所需要的资源。

2. User Entity

2.用户实体

First – let’s put forward the simple entity that we’re going to use for our filter/search API – a basic User:

首先,让我们提出一个简单的实体,我们将用于我们的过滤器/搜索API – 一个基本的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. Filter Using CriteriaBuilder

3.使用CriteriaBuilder过滤

Now – let’s get into the meat of the problem – the query in the persistence layer.

现在–让我们进入问题的实质–持久层的查询。

Building a query abstraction is a matter of balance. We need a good amount of flexibility on the one hand, and we need to keep complexity manageable on the other. High level, the functionality is simple – you pass in some constraints and you get back some results.

构建一个查询抽象是一个平衡的问题。一方面,我们需要大量的灵活性,另一方面,我们需要保持可管理的复杂性。在高层次上,功能很简单–你传入一些约束条件,你得到一些结果

Let’s see how that works:

让我们看看效果如何。

@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<User> result = entityManager.createQuery(query).getResultList();
        return result;
    }

    @Override
    public void save(User entity) {
        entityManager.persist(entity);
    }
}

Let’s have a look at the UserSearchQueryCriteriaConsumer class:

让我们看一下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()));
            }
        }
    }

    // standard constructor, getter, setter
}

As you can see, the searchUser API takes a list of very simple constraints, composes a query based on these constraints, does the search and returns the results.

正如你所看到的,searchUser API需要一个非常简单的约束条件列表,根据这些约束条件组成一个查询,进行搜索并返回结果。

The constraint class is quite simple as well:

约束类也很简单。

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

The SearchCriteria implementation holds our Query parameters:

SearchCriteria实现持有我们的Query参数。

  • key: used to hold field name – for example: firstName, age, … etc.
  • operation: used to hold the operation – for example: Equality, less than, … etc.
  • value: used to hold the field value – for example: john, 25, … etc.

4. Test the Search Queries

4.测试搜索查询

Now – let’s test our search mechanism to make sure it holds water.

现在–让我们测试一下我们的搜索机制,以确保它有水分。

First – let’s initialize our database for testing by adding two users – as in the following example:

首先–让我们通过添加两个用户来初始化我们的数据库进行测试–如下面的例子。

@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("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("tom@doe.com");
        userTom.setAge(26);
        userApi.save(userTom);
    }
}

Now, let’s get a User with specific firstName and lastName – as in the following example:

现在,让我们得到一个具有特定firstNamelastNameUser–如以下例子。

@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)));
}

Next, let’s get a List of User with the same lastName:

接下来,让我们得到一个List具有相同lastNameUser

@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));
}

Next, let’s get users with age greater than or equal 25:

接下来,让我们获得年龄大于或等于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)));
}

Next, let’s search for users that don’t actually exist:

接下来,让我们搜索一下实际上不存在的用户

@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)));
}

Finally, let’s search for users given only partial firstName:

最后,让我们搜索一下只给出部分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. The UserController

6.UserController

Finally, let’s now wire in the persistence support for this flexible search to our REST API.

最后,让我们现在把这个灵活的搜索的持久性支持连接到我们的REST API。

We’re going to be setting up a simple UserController – with a findAll() using the “search” to pass in the entire search/filter expression:

我们将设置一个简单的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);
    }
}

Note how we’re simply creating our search criteria objects out of the search expression.

注意我们是如何简单地从搜索表达式中创建我们的搜索标准对象的。

We’re now at the point where we can start playing with the API and make sure everything is working correctly:

我们现在已经到了可以开始使用API的地步,并确保一切工作正常。

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

And here is its response:

这里是它的回应。

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

7. Conclusion

7.结论

This simple yet powerful implementation enables quite a bit of smart filtering on a REST API. Yes – it’s still rough around the edges and can be improved (and will be improved in the next article) – but it’s a solid starting point to implement this kind of filtering functionality on your APIs.

这个简单而强大的实现可以在REST API上实现相当多的智能过滤功能。是的–它的边缘还很粗糙,可以改进(在下一篇文章中会有改进)–但这是在你的API上实现这种过滤功能的一个坚实的起点。

The full implementation of this article can be found in the GitHub project.

本文的完整实现可以在GitHub项目中找到。

Next »

REST Query Language with Spring Data JPA Specifications