REST Query Language with RSQL – 使用RSQL的REST查询语言

最后修改: 2015年 4月 11日

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

1. Overview

1.概述

In this fifth article of the series we’ll illustrate building the REST API Query language with the help of a cool library – rsql-parser.

在这篇系列文章的第五篇中,我们将说明在一个很酷的库–rsql-parser的帮助下构建REST API查询语言。

RSQL is a super-set of the Feed Item Query Language (FIQL) – a clean and simple filter syntax for feeds; so it fits quite naturally into a REST API.

RSQL是Feed Item Query Language(FIQL)的一个超级集。- 是一种简洁的Feeds过滤语法;因此它非常自然地适合于REST API。

2. Preparations

2.准备工作

First, let’s add a Maven dependency to the library:

首先,让我们为该库添加一个Maven依赖项

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

And also define the main entity we’re going to be working with throughout the examples – User:

还有定义我们将在整个例子中使用的主要实体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. Parse the Request

3.解析请求

The way RSQL expressions are represented internally is in the form of nodes and the visitor pattern is used to parse out the input.

RSQL表达式的内部表示方式是以节点的形式出现的,访问者模式被用来解析出输入。

With that in mind, we’re going to implement the RSQLVisitor interface and create our own visitor implementation – CustomRsqlVisitor:

考虑到这一点,我们将实现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);
    }
}

Now we need to deal with persistence and construct our query out of each of these nodes.

现在我们需要处理持久性问题,并从这些节点中的每一个节点构建我们的查询。

We’re going to use the Spring Data JPA Specifications we used before – and we’re going to implement a Specification builder to construct Specifications out of each of these nodes we visit:

我们将使用Spring Data JPA规范我们之前使用的 – 我们将实现一个Specification构建器来从我们访问的每个节点中构建规范

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

Note how:

注意如何。

  • LogicalNode is an AND/OR Node and has multiple children
  • ComparisonNode has no children and it holds the Selector, Operator and the Arguments

For example, for a query “name==john” – we have:

例如,对于一个查询”name==john” – 我们有。

  1. Selector: “name”
  2. Operator: “==”
  3. Arguments:[john]

4. Create Custom Specification

4.创建自定义规格

When constructing the query we made use of a Specification:

在构建查询时,我们使用了一个规格:

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
}

Notice how the spec is using generics and isn’t tied to any specific Entity (such as the User).

请注意该规范是如何使用泛型的,并不与任何特定的实体(如用户)相联系。

Next – here’s our enum “RsqlSearchOperation which holds default rsql-parser operators:

接下来–这里是我们的enum “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. Test Search Queries

5.测试搜索查询

Let’s now start testing our new and flexible operations through some real-world scenarios:

现在让我们开始通过一些真实世界的场景测试我们新的和灵活的操作。

First – let’s initialize the data:

首先–让我们来初始化数据。

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

Now let’s test the different operations:

现在我们来测试一下不同的操作。

5.1. Test Equality

5.1.测试平等性

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

在下面的例子中–我们将通过他们的来搜索用户。

@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. Test Negation

5.2.测试否定法

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

接下来,让我们通过他们的而不是 “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. Test Greater Than

5.3.大于测试

Next – we will search for users with age greater than “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. Test Like

5.4.类似测试

Next – we will search for users with their first name starting with “jo”:

接下来–我们将搜索用户的以”jo“开头。

@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. Test IN

5.5.测试in

Next – we will search for users their first name is “john” or “jack“:

接下来–我们将搜索用户的名字是”john“或”jack“。

@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

6.UserController

Finally – let’s tie it all in with the controller:

最后–让我们把这一切与控制器联系起来。

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

Here’s a sample URL:

这里有一个URL样本。

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. Conclusion

7.结论

This tutorial illustrated how to build out a Query/Search Language for a REST API without having to re-invent the syntax and instead using FIQL / RSQL.

本教程说明了如何为REST API建立一个查询/搜索语言,而不必重新发明语法,而是使用FIQL/RSQL。

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 with Querydsl Web Support

« Previous

REST Query Language – Implementing OR Operation