RSQLによるRESTクエリ言語
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
”の場合 –
-
セレクタ
:“名前” -
演算子
:“ ==” -
引数
:[ジョン]
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ベースのプロジェクトなので、インポートしてそのまま実行するのは簡単なはずです。
次
”
-
«** 前へ