1概要

リンクの5番目の記事:/spring-rest-api-query-search-language-tutorial[シリーズ]では、

クールなライブラリ –

https://githubの助けを借りてREST

APIクエリー言語を構築する方法を説明します。 com/jirutka/rsql-parser[rsql-parser]。

RSQLは、フィード項目クエリ言語(http://tools.ietf.org/html/draft-nottingham-atompub-fiql-00[FIQL])のスーパーセットです – フィード用のクリーンでシンプルなフィルタ構文。それでそれはREST APIに非常に自然に収まります。


2準備

まず、ライブラリにMavenの依存関係を追加しましょう。

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

そして

私たちが例を通して扱うことになるだろう

主な実体** を定義します –

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リクエストを解析する

RSQL式が内部的に表現される方法はノードの形式であり、訪問者パターンは入力を解析するために使用されます。

そのことを念頭に置いて、https://github.com/jirutka/rsql-parser/blob/master/src/main/java/cz/jirutka/rsql/parser/ast/RSQLVisitor.java[

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仕様リンクを使うつもりです:/rest-api-search-language-spring-data-specification[私たちは以前に使用しました] – そしてそれぞれから仕様を構築するために

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

方法に注意してください。


  • LogicalNode




    AND



    __/



    OR


    Node__であり、複数の子を持ちます。


  • ComparisonNode

    は子を持たず、** セレクタ、演算子を保持します。

と引数**

たとえば、クエリ“

name == john

”の場合 –


  1. セレクタ

    :“名前”


  2. 演算子

    :“ ==”


  3. 引数

    :[ジョン]


4カスタム

仕様


を作成

クエリを構築するときに、__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
}

仕様がジェネリックを使用しており、特定のエンティティ(ユーザーなど)に関連付けられていないことに注意してください。

次に、これがデフォルトのrsql-parser演算子を保持する

enum“

RsqlSearchOperation



です。

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. テスト否定

次に、「ファーストネーム」で「ジョン」ではないユーザーを検索しましょう。

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

より大きいテスト

次に、

age

が「

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. いいねテスト

次に、最初の名前が「

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. テスト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:8080/users?search=firstName==jo** ;age<25

そして応答:

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


7. 結論

このチュートリアルでは、構文を作り直すことなく、代わりにFIQL/RSQLを使用しなくても、REST API用のクエリ/検索言語を構築する方法を説明しました。

この記事の

完全な実装

はhttps://github.com/eugenp/tutorials/tree/master/spring-rest-query-language[the GitHub project]にあります – これはMavenベースのプロジェクトなので、インポートしてそのまま実行するのは簡単なはずです。




  • «** 前へ