RSQLを使用したRESTクエリ言語

RESTトップ

Spring5とSpringBoot2の基礎に焦点を当てた新しいLearnSpringコースを発表しました。

>>コース持続性トップを チェックしてください

Spring5とSpringBoot2の基礎に焦点を当てた新しいLearnSpringコースを発表しました。

>>コースをチェックしてくださいこの記事はシリーズの一部です:•SpringおよびJPA基準を使用したRESTクエリ言語

•SpringDataJPA仕様を使用したRESTクエリ言語

•SpringDataJPAおよびQuerydslを使用したRESTクエリ言語

•RESTクエリ言語–高度な検索操作

•RESTクエリ言語–OR操作の実装

•RSQLを使用したRESTクエリ言語(現在の記事)•QuerydslWebサポートを使用したRESTクエリ言語

1。概要

シリーズのこの5番目の記事では、クールなライブラリrsql-parserを使用してRESTAPIクエリ言語を構築する方法を説明します

RSQLは、フィードアイテムクエリ言語(FIQL)のスーパーセットです。これは、フィード用のクリーンでシンプルなフィルター構文です。そのため、RESTAPIに非常に自然に適合します。

2.準備

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

 cz.jirutka.rsql rsql-parser 2.1.0 

また、例全体で使用するメインエンティティ定義しますユーザー

@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 implements RSQLVisitor
     
       { private GenericRsqlSpecBuilder builder; public CustomRsqlVisitor() { builder = new GenericRsqlSpecBuilder(); } @Override public Specification visit(AndNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(OrNode node, Void param) { return builder.createSpecification(node); } @Override public Specification visit(ComparisonNode node, Void params) { return builder.createSecification(node); } }
     

次に、永続性を処理し、これらの各ノードからクエリを作成する必要があります。

以前に使用したSpringData JPA仕様を使用し、仕様ビルダーを実装して、アクセスするこれらの各ノードから仕様構築します。

public class GenericRsqlSpecBuilder { public Specification createSpecification(Node node) { if (node instanceof LogicalNode) { return createSpecification((LogicalNode) node); } if (node instanceof ComparisonNode) { return createSpecification((ComparisonNode) node); } return null; } public Specification createSpecification(LogicalNode logicalNode) { List specs = logicalNode.getChildren() .stream() .map(node -> createSpecification(node)) .filter(Objects::nonNull) .collect(Collectors.toList()); Specification 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 createSpecification(ComparisonNode comparisonNode) { Specification result = Specification.where( new GenericRsqlSpecification( comparisonNode.getSelector(), comparisonNode.getOperator(), comparisonNode.getArguments() ) ); return result; } }

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

  • LogicalNodeAND / ORノードであり、複数の子があります
  • ComparisonNodeには子がなく、セレクター、演算子、および引数を保持します

たとえば、クエリ「name == john」の場合、次のようになります。

  1. セレクター:「名前」
  2. 演算子:“ ==”
  3. 引数:[john]

4.カスタム仕様を作成します

クエリを作成するときに、仕様を利用しました

public class GenericRsqlSpecification implements Specification { private String property; private ComparisonOperator operator; private List arguments; @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { List 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. 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. get(property), argument.toString()); } case GREATER_THAN_OR_EQUAL: { return builder.greaterThanOrEqualTo(root. get(property), argument.toString()); } case LESS_THAN: { return builder.lessThan(root. get(property), argument.toString()); } case LESS_THAN_OR_EQUAL: { return builder.lessThanOrEqualTo(root. 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 castArguments(final Root root) { Class type = root.get(property).getJavaType(); List 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演算子を保持する列挙型「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 spec = rootNode.accept(new CustomRsqlVisitor()); List 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 spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userTom, isIn(results)); assertThat(userJohn, not(isIn(results))); }

5.3。大なり記号をテストする

次へ– 「25」を超える年齢のユーザーを検索します。

@Test public void givenMinAge_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("age>25"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List 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 spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

5.5。テストイン

次に、ユーザーのが「john」または「jack」であるユーザーを検索します。

@Test public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); Specification spec = rootNode.accept(new CustomRsqlVisitor()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }

6. UserController

最後に、すべてをコントローラーに結び付けましょう。

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

サンプルURLは次のとおりです。

//localhost:8080/users?search=firstName==jo*;age<25

そして応答:

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

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.

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.

Next » REST Query Language with Querydsl Web Support « Previous REST Query Language – Implementing OR Operation REST bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE