知識庫 / Spring RSS 訂閱

RSQL:REST 查詢語言

Persistence,REST,Spring
HongKong
4
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>
   • REST 查詢語言 – 高級搜索操作
   <br>
   • REST 查詢語言 – 實現 OR 操作
   <br>
   <div>
    • 使用 RSQL 的 REST 查詢語言 (當前文章)
   </div >
   • REST 查詢語言與 Querydsl Web 支持
   <br>
   </div >
  <div>
  </div>
 </div>
 <!-- .article-series-links -->
</div >
<!-- end of article series section -->

1. 概述

在本文檔系列中的第五篇,我們將通過使用 rsql-parser (https://github.com/jirutka/rsql-parser) 來演示構建 REST API 查詢語言。

RSQL 是 Feed Item 查詢語言 (FIQL) 的超集 (FIQL) – 這是一個用於過濾 Feeds 的簡潔易用的語法;因此,它自然地融入到 REST API 中。

2. 準備工作

首先,我們需要將一個 Maven 依賴項添加到庫中:

<dependency>
    <groupId>cz.jirutka.rsql</groupId>
    <artifactId>rsql-parser</artifactId>
    <version>2.1.0</version>
</dependency>

並且同時定義我們將會在所有示例中使用的主要實體——用户

@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. 解析請求

RSQL表達式的內部表示形式為節點,並使用訪問者模式進行解析。

考慮到這一點,我們將實現 RSQLVisitor 接口,並創建我們自己的訪問者實現 – CustomRsqlVisitor:

public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {

    private GenericRsqlSpecBuilder<T> builder;

    public CustomRsqlVisitor() {
        builder = new GenericRsqlSpecBuilder<T>();
    }

    @Override
    public Specification<T> visit(AndNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(OrNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(ComparisonNode node, Void params) {
        return builder.createSecification(node);
    }
}

現在我們需要處理持久化,並根據這些節點構建查詢。

我們將使用之前使用的 Spring Data JPA Specifications,並且我們將實現一個 Specification 構造器,用於 從我們訪問的每個節點構建 Specifications

public class GenericRsqlSpecBuilder<T> {

    public Specification<T> createSpecification(Node node) {
        if (node instanceof LogicalNode) {
            return createSpecification((LogicalNode) node);
        }
        if (node instanceof ComparisonNode) {
            return createSpecification((ComparisonNode) node);
        }
        return null;
    }

    public Specification<T> createSpecification(LogicalNode logicalNode) {        
        List<Specification> specs = logicalNode.getChildren()
          .stream()
          .map(node -> createSpecification(node))
          .filter(Objects::nonNull)
          .collect(Collectors.toList());

        Specification<T> result = specs.get(0);
        if (logicalNode.getOperator() == LogicalOperator.AND) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).and(specs.get(i));
            }
        } else if (logicalNode.getOperator() == LogicalOperator.OR) {
            for (int i = 1; i < specs.size(); i++) {
                result = Specification.where(result).or(specs.get(i));
            }
        }

        return result;
    }

    public Specification<T> createSpecification(ComparisonNode comparisonNode) {
        Specification<T> result = Specification.where(
          new GenericRsqlSpecification<T>(
            comparisonNode.getSelector(), 
            comparisonNode.getOperator(), 
            comparisonNode.getArguments()
          )
        );
        return result;
    }
}

請注意以下內容:

  • LogicalNode 是一個 AND/OR Node,並且具有多個子節點
  • ComparisonNode 沒有子節點,它存儲了 選擇器、運算符和參數

例如,對於查詢“name==john” – 我們有:

  1. 選擇器: “name”
  2. 運算符: “==”
  3. 參數:[john]

4. 創建自定義規範

在構建查詢時,我們使用了自定義規範。

public class GenericRsqlSpecification<T> implements Specification<T> {

    private String property;
    private ComparisonOperator operator;
    private List<String> arguments;

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        List<Object> args = castArguments(root);
        Object argument = args.get(0);
        switch (RsqlSearchOperation.getSimpleOperator(operator)) {

        case EQUAL: {
            if (argument instanceof String) {
                return builder.like(root.get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNull(root.get(property));
            } else {
                return builder.equal(root.get(property), argument);
            }
        }
        case NOT_EQUAL: {
            if (argument instanceof String) {
                return builder.notLike(root.<String> get(property), argument.toString().replace('*', '%'));
            } else if (argument == null) {
                return builder.isNotNull(root.get(property));
            } else {
                return builder.notEqual(root.get(property), argument);
            }
        }
        case GREATER_THAN: {
            return builder.greaterThan(root.<String> get(property), argument.toString());
        }
        case GREATER_THAN_OR_EQUAL: {
            return builder.greaterThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case LESS_THAN: {
            return builder.lessThan(root.<String> get(property), argument.toString());
        }
        case LESS_THAN_OR_EQUAL: {
            return builder.lessThanOrEqualTo(root.<String> get(property), argument.toString());
        }
        case IN:
            return root.get(property).in(args);
        case NOT_IN:
            return builder.not(root.get(property).in(args));
        }

        return null;
    }

    private List<Object> castArguments(final Root<T> root) {
        
        Class<? extends Object> type = root.get(property).getJavaType();
        
        List<Object> args = arguments.stream().map(arg -> {
            if (type.equals(Integer.class)) {
               return Integer.parseInt(arg);
            } else if (type.equals(Long.class)) {
               return Long.parseLong(arg);
            } else {
                return arg;
            }            
        }).collect(Collectors.toList());

        return args;
    }

    // standard constructor, getter, setter
}

請注意,規範中使用了泛型,並且沒有與特定的實體(如用户)綁定。

接下來,這裏是我們的 枚舉 “RsqlSearchOperation,它包含默認的 rsql-parser 運算符:

public enum RsqlSearchOperation {
    EQUAL(RSQLOperators.EQUAL), 
    NOT_EQUAL(RSQLOperators.NOT_EQUAL), 
    GREATER_THAN(RSQLOperators.GREATER_THAN), 
    GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), 
    LESS_THAN(RSQLOperators.LESS_THAN), 
    LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), 
    IN(RSQLOperators.IN), 
    NOT_IN(RSQLOperators.NOT_IN);

    private ComparisonOperator operator;

    private RsqlSearchOperation(ComparisonOperator operator) {
        this.operator = operator;
    }

    public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
        for (RsqlSearchOperation operation : values()) {
            if (operation.getOperator() == operator) {
                return operation;
            }
        }
        return null;
    }
}

5. 測試搜索查詢

現在,讓我們通過一些實際場景來測試我們新的、靈活的操作:

首先,讓我們初始化數據:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class RsqlTest {

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

現在我們來測試不同的操作:

5.1. 比較相等性

在下面的示例中,我們將通過用户的 進行搜索:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

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

5.2. 否定測試

接下來,我們搜索那些名字的第一名不等於“john”的用户:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName!=john");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

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

5.3. 查找年齡大於“25”的用户

接下來,我們將搜索年齡大於“25”的用户:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("age>25");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

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

5.4. 模擬測試

接下來,我們將搜索擁有“first name</em style="font-style: italic;">姓名的用户,其名字的前綴為“jo</em style="font-style: italic;">”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==jo*");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

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

5.5. 測試 <em IN

接下來,我們將搜索用户,他們的 是 “” 或 “”:

@Test
public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

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

6. UserController

最後,讓我們將所有內容與控制器聯繫起來:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllByRsql(@RequestParam(value = "search") String search) {
    Node rootNode = new RSQLParser().parse(search);
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    return dao.findAll(spec);
}

以下是一個示例URL:

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. 結論

本教程闡述瞭如何在 REST API 中構建 Query/搜索語言的方法,無需重新發明語法,而是利用 FIQL / RSQL。

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

發佈 評論

Some HTML is okay.