REST Query Language – Advanced Search Operations – REST查询语言 – 高级搜索操作

最后修改: 2015年 2月 21日

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

1. Overview

1.概述

In this article, we’ll extend the REST Query Language we developed in the previous parts of the series to include more search operations.

在本文中,我们将扩展我们在该系列的前几部分中开发的REST查询语言,以包含更多搜索操作

We now support the following operations: Equality, Negation, Greater than, Less than, Starts with, Ends with, Contains and Like.

我们现在支持以下操作。等价、否定、大于、小于、开始于、结束于、包含和类似。

Note that we explored three implementations – JPA Criteria, Spring Data JPA Specifications and Query DSL; we’re going forward with Specifications in this article because it’s a clean and flexible way to represent our operations.

请注意,我们探索了三种实现方式–JPA标准、Spring Data JPA规范和查询DSL;在本文中,我们将继续使用规范,因为它是表示我们操作的一种简洁而灵活的方式。

2. The SearchOperation enum

2、SearchOperation enum

First – let’s start by defining a better representation of our various supported search operations – via an enumeration:

首先–让我们开始定义一个更好的代表我们的各种支持的搜索操作–通过一个枚举。

public enum SearchOperation {
    EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS;

    public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" };

    public static SearchOperation getSimpleOperation(char input) {
        switch (input) {
        case ':':
            return EQUALITY;
        case '!':
            return NEGATION;
        case '>':
            return GREATER_THAN;
        case '<':
            return LESS_THAN;
        case '~':
            return LIKE;
        default:
            return null;
        }
    }
}

We have two sets of operations:

我们有两套操作。

1. Simple – can be represented by one character:

1.简单 – 可以用一个字符表示。

  • Equality: represented by colon (:)
  • Negation: represented by Exclamation mark (!)
  • Greater than: represented by (>)
  • Less than: represented by (<)
  • Like: represented by tilde (~)

2. Complex – need more than one character to be represented:

2.复杂的 – 需要一个以上的字符来表示。

  • Starts with: represented by (=prefix*)
  • Ends with: represented by (=*suffix)
  • Contains: represented by (=*substring*)

We also need to modify our SearchCriteria class to use the new SearchOperation:

我们还需要修改我们的 SearchCriteria 类以使用新的 SearchOperation

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

3. Modify UserSpecification

3.修改UserSpecification

Now – let’s include the newly supported operations into our UserSpecification implementation:

现在–让我们把新支持的操作纳入我们的UserSpecification实现。

public class UserSpecification implements Specification<User> {

    private SearchCriteria criteria;

    @Override
    public Predicate toPredicate(
      Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
    
        switch (criteria.getOperation()) {
        case EQUALITY:
            return builder.equal(root.get(criteria.getKey()), criteria.getValue());
        case NEGATION:
            return builder.notEqual(root.get(criteria.getKey()), criteria.getValue());
        case GREATER_THAN:
            return builder.greaterThan(root.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case LESS_THAN:
            return builder.lessThan(root.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case LIKE:
            return builder.like(root.<String> get(
              criteria.getKey()), criteria.getValue().toString());
        case STARTS_WITH:
            return builder.like(root.<String> get(criteria.getKey()), criteria.getValue() + "%");
        case ENDS_WITH:
            return builder.like(root.<String> get(criteria.getKey()), "%" + criteria.getValue());
        case CONTAINS:
            return builder.like(root.<String> get(
              criteria.getKey()), "%" + criteria.getValue() + "%");
        default:
            return null;
        }
    }
}

4. Persistence Tests

4.持久性测试

Next – we let’s test our new search operations – at the persistence level:

接下来–我们来测试一下我们的新搜索操作–在持久化层面上。

4.1. Test Equality

4.1.测试平等性

In the following example – we’ll search for a user by their first and last name:

在下面的例子中–我们将通过名字和姓氏来搜索一个用户

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

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

4.2. Test Negation

4.2.测试否定法

Next, let’s search for users that by the their first name not “john”:

接下来,让我们通过他们的名字不是 “john”/strong>来搜索用户。

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.NEGATION, "john"));
    List<User> results = repository.findAll(Specification.where(spec));

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

4.3. Test Greater Than

4.3.大于测试

Next – we will search for users with age greater than “25”:

接下来–我们将搜索年龄大于 “25 “的用户

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "25"));
    List<User> results = repository.findAll(Specification.where(spec));

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

4.4. Test Starts With

4.4.测试以开始

Next – users with their first name starting with “jo”:

下一个–名字以 “jo “开头的用户

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.STARTS_WITH, "jo"));
    List<User> results = repository.findAll(spec);

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

4.5. Test Ends With

4.5 测试结束

Next we’ll search for users with their first name ending with “n”:

接下来,我们将搜索以名字以 “n “结尾的用户

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.ENDS_WITH, "n"));
    List<User> results = repository.findAll(spec);

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

4.6. Test Contains

4.6.测试包含

Now, we’ll search for users with their first name containing “oh”:

现在,我们将搜索名字中含有 “oh “的用户

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("firstName", SearchOperation.CONTAINS, "oh"));
    List<User> results = repository.findAll(spec);

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

4.7. Test Range

4.7.测试范围

Finally, we’ll search for users with ages between “20” and “25”:

最后,我们将搜索年龄在 “20 “和 “25 “之间的用户

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    UserSpecification spec = new UserSpecification(
      new SearchCriteria("age", SearchOperation.GREATER_THAN, "20"));
    UserSpecification spec1 = new UserSpecification(
      new SearchCriteria("age", SearchOperation.LESS_THAN, "25"));
    List<User> results = repository.findAll(Specification.where(spec).and(spec1));

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

5. The UserSpecificationBuilder

5.UserSpecificationBuilder

Now that persistence is done and tested, let’s move our attention to the web layer.

现在,持久性已经完成并经过测试,让我们把注意力转移到网络层。

We’ll build on top of the UserSpecificationBuilder implementation from the previous article to incorporate the new new search operations:

我们将建立在上一篇文章中的UserSpecificationBuilder实现之上,以纳入新的新搜索操作

public class UserSpecificationsBuilder {

    private List<SearchCriteria> params;

    public UserSpecificationsBuilder with(
      String key, String operation, Object value, String prefix, String suffix) {
    
        SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
        if (op != null) {
            if (op == SearchOperation.EQUALITY) {
                boolean startWithAsterisk = prefix.contains("*");
                boolean endWithAsterisk = suffix.contains("*");

                if (startWithAsterisk && endWithAsterisk) {
                    op = SearchOperation.CONTAINS;
                } else if (startWithAsterisk) {
                    op = SearchOperation.ENDS_WITH;
                } else if (endWithAsterisk) {
                    op = SearchOperation.STARTS_WITH;
                }
            }
            params.add(new SearchCriteria(key, op, value));
        }
        return this;
    }

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

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

        return result;
    }
}

6. The UserController

6.UserController

Next – we need to modify our UserController to correctly parse the new operations:

接下来–我们需要修改我们的UserController,以正确地解析新操作

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllBySpecification(@RequestParam(value = "search") String search) {
    UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
    String operationSetExper = Joiner.on("|").join(SearchOperation.SIMPLE_OPERATION_SET);
    Pattern pattern = Pattern.compile(
      "(\\w+?)(" + operationSetExper + ")(\p{Punct}?)(\\w+?)(\p{Punct}?),");
    Matcher matcher = pattern.matcher(search + ",");
    while (matcher.find()) {
        builder.with(
          matcher.group(1), 
          matcher.group(2), 
          matcher.group(4), 
          matcher.group(3), 
          matcher.group(5));
    }

    Specification<User> spec = builder.build();
    return dao.findAll(spec);
}

We can now hit the API and get back the right results with any combination of criteria. For example – here’s a what a complex operation would look like using API with the query language:

我们现在可以点击API,用任何标准的组合获得正确的结果。例如–这里是一个复杂的操作,使用API的查询语言会是什么样子。

http://localhost:8080/users?search=firstName:jo*,age<25

And the response:

而回应是。

[{
    "id":1,
    "firstName":"john",
    "lastName":"doe",
    "email":"john@doe.com",
    "age":24
}]

7. Tests for the Search API

7.搜索API的测试

Finally – let’s make sure our API works well by writing a suite of API tests.

最后–让我们通过编写一套API测试来确保我们的API运行良好。

We’ll start with the simple configuration of the test and the data initialization:

我们将从测试的简单配置和数据初始化开始。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = { ConfigTest.class, PersistenceConfig.class }, 
  loader = AnnotationConfigContextLoader.class)
@ActiveProfiles("test")
public class JPASpecificationLiveTest {

    @Autowired
    private UserRepository repository;

    private User userJohn;
    private User userTom;

    private final String URL_PREFIX = "http://localhost:8080/users?search=";

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

    private RequestSpecification givenAuth() {
        return RestAssured.given().auth()
                                  .preemptive()
                                  .basic("username", "password");
    }
}

7.1. Test Equality

7.1.测试平等性

First – let’s search for a user with the first name “john” and last name “doe:

首先–让我们搜索一个名字为”john“、姓氏为”doe“的用户。

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:john,lastName:doe");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.2. Test Negation

7.2.测试否定法

Now – we’ll search for users when their first name isn’t “john”:

现在–我们将在他们的名字不是 “john”时搜索用户。

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName!john");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.3. Test Greater Than

7.3.大于的测试

Next – we will look for users with age greater than “25”:

接下来–我们将寻找年龄大于 “25 “的用户

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>25");
    String result = response.body().asString();

    assertTrue(result.contains(userTom.getEmail()));
    assertFalse(result.contains(userJohn.getEmail()));
}

7.4. Test Starts With

7.4.测试以开始

Next – users with their first name starting with “jo”:

下一个–名字以 “jo “开头的用户

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:jo*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.5. Test Ends With

7.5 测试结束

Now – users with their first name ending with “n”:

现在–用户的名字以 “n “结尾的他们的名字

@Test
public void givenFirstNameSuffix_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*n");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.6. Test Contains

7.6.测试包括

Next, we’ll search for users with their first name containing “oh”:

接下来,我们将搜索名字中含有 “oh “的用户

@Test
public void givenFirstNameSubstring_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "firstName:*oh*");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

7.7. Test Range

7.7.测试范围

Finally, we’ll search for users with ages between “20” and “25”:

最后,我们将搜索年龄在 “20 “和 “25 “之间的用户

@Test
public void givenAgeRange_whenGettingListOfUsers_thenCorrect() {
    Response response = givenAuth().get(URL_PREFIX + "age>20,age<25");
    String result = response.body().asString();

    assertTrue(result.contains(userJohn.getEmail()));
    assertFalse(result.contains(userTom.getEmail()));
}

8. Conclusion

8.结论

In this article we brought the query language of our REST Search API forward to a mature, tested, production-grade implementation. We now support a wide variety of operations and constraints, which should make it quite easy to cut across any dataset elegantly and get to the exact resources we’re looking for.

在这篇文章中,我们把我们的REST搜索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 – Implementing OR Operation

« Previous

REST Query Language with Spring Data JPA and Querydsl