知識庫 / Spring RSS 訂閱

REST 查詢語言 – 高級搜索操作

Persistence,REST,Spring
HongKong
7
04:11 AM · Dec 06 ,2025
<div>
 <a class="article-series-header" href="javascript:void(0);">這篇文檔是系列文章的一部分</a>
 <div>
  <div>
   • 使用 Spring 和 JPA Criteria 構建 REST 查詢語言
   <br>
   • 使用 Spring Data JPA Specifications 構建 REST 查詢語言
   <br>
   • 使用 Spring Data JPA 和 Querydsl 構建 REST 查詢語言
   <br>
   <div>
    • 高級搜索操作 – REST 查詢語言 (當前文章)
   </div>
   • REST 查詢語言 – 實現 OR 運算
   <br>
   • REST 查詢語言與 RSQL 的結合
   <br>
   • REST 查詢語言與 Querydsl Web 支持
   <br>
  </div>
  <!-- end of article series inner -->
 </div>
 <!-- .article-series-links -->
</div>
<!-- end of article series section -->

1. 概述

在本文中,我們將擴展我們在上一系列文章中開發的 REST 查詢語言,以包含更多搜索操作

我們現在支持以下操作:相等、否定、大於、小於、以...開頭、以...結尾、包含和 LIKE。

請注意,我們探索了三種實現方案——JPA Criteria、Spring Data JPA Specifications 和 Query DSL;在本文中,我們將繼續使用 Specifications,因為它是一種清晰且靈活的方式來表示我們的操作。

2. SearchOperation 枚舉

首先,讓我們通過枚舉來定義對我們支持的各種搜索操作的更好表示:

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

我們有兩組操作:

1. 簡單 – 可以用一個字符表示:

  • 相等:用冒號 (:) 表示
  • 否定:用感嘆號 (!) 表示
  • 大於:用大於號 (>) 表示
  • 小於:用小於號 (<) 表示
  • 相似:用波浪線 (~) 表示

2. 複雜 – 需要使用多個字符來表示:

  • 以...開始:用 (=prefix*) 表示
  • 以...結束:用 (=*suffix) 表示
  • 包含...:用 (=*substring*) 表示

我們還需要修改我們的 SearchCriteria 類,以使用新的 SearchOperation

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

3. 修改 UserSpecification

現在,讓我們將新支持的操作包含到我們的 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. 存儲持久性測試

接下來,我們測試新的搜索操作 – 在存儲持久性級別上:

4.1. 比較相等性

在下面的示例中,我們將通過 用户名及其姓和名 來搜索用户:

@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. 否定測試

接下來,我們搜索那些名字中 不包含“john” 的用户:

@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. 查找年齡大於“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. 測試從…開始

用户,其名字首字母為“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. 測試結束於

接下來,我們將搜索所有其姓名為以“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. 搜索包含內容

現在,我們將搜索所有姓名為包含“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. 測試範圍

最後,我們將搜索年齡在“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

現在持久化已完成並經過測試,我們把注意力轉向 Web 層。

我們將在此基礎上,利用上一篇文章中 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. UserController

接下來,我們需要修改我們的 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);
}

我們現在可以調用API並獲得任何組合條件的正確結果。例如,以下是一個使用API和查詢語言構建的複雜操作的示例:

http://localhost:8082/spring-rest-query-language/auth/users?search=firstName:jo*,age<25

以及響應:

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

7. 搜索API測試

讓我們通過編寫API測試套件來確保我們的API正常工作。

首先,我們將配置測試以及數據初始化:

@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:8082/spring-rest-query-language/auth/users?search=";

    @Before
    public void init() {
        userJohn = new User();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repository.save(userJohn);

        userTom = new User();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repository.save(userTom);
    }

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

7.1. 比較相等性

首先,讓我們搜索一個名字為 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. 測試否定

現在,我們將搜索那些其名字不是“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. 比較大於 </h3

接下來,我們將查找年齡大於“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. 測試從…開始

用户,其名字首字母為“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. 測試結束時

現在 – 姓名以“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. 測試包含

接下來,我們將搜索所有名字包含“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. 測試範圍

最後,我們將搜索年齡在“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. 結論

在本文中,我們已將 REST Search API 的查詢語言向前推進到 成熟、經過測試的生產級實現。我們現在支持各種各樣的操作和約束,這應該使我們能夠優雅地跨越任何數據集,並獲取我們正在尋找的精確資源。

下一條 »
REST 查詢語言 – 實現 OR 操作
« 上一頁
REST 查詢語言與 Spring Data JPA 和 Querydsl
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.