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

Persistence,REST,Spring
Remote
0
08:16 PM · Dec 01 ,2025

1. 概述

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

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

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

2. The 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(spec);
    
        assertThat(userTom, isIn(results));
        assertThat(userJohn, not(isIn(results)));
    }

    4.3. 測試大於

    接下來,我們將搜索年齡大於“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. 用户規格構建器

    現在持久化已完成並經過測試,讓我們將注意力集中在 Web 層上。

    我們將在此前文章的 用户規格構建器 實現上進行擴展,以 集成新的搜索操作

    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

    接下來,我們需要修改我們的 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. Tests for the Search API

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

    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: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. Test Equality

    First – let’s search for a user with the first name “john” and last name “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

    Now – we’ll search for users when their first name isn’t “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

    Next – we will look for users with age greater than “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

    Next – users with their first name starting with “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

    Now – users with their first name ending with “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

    Next, we’ll search for users with their first name containing “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

    Finally, we’ll search for users with ages between “20” and “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.