RSQLを使用したRESTクエリ言語
1. 概要
the seriesのこの5番目の記事では、a cool library – rsql-parser.を使用してRESTAPIクエリ言語を構築する方法を説明します。
RSQLは、フィードアイテムクエリ言語(FIQL)のスーパーセットです。これは、フィード用のクリーンでシンプルなフィルター構文です。そのため、RESTAPIに非常に自然に適合します。 **
2. 準備する
まず、ライブラリにMavenの依存関係を追加しましょう。
cz.jirutka.rsql
rsql-parser
2.0.0
また、例全体で使用するdefine the main entity –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式が内部的に表現される方法はノードの形式であり、ビジターパターンを使用して入力を解析します。
そのことを念頭に置いて、RSQLVisitor interfaceを実装し、独自の訪問者実装–CustomRsqlVisitorを作成します。
public class CustomRsqlVisitor implements RSQLVisitor, Void> {
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);
}
}
ここで、永続性を処理し、これらの各ノードからクエリを構築する必要があります。
Spring Data JPA仕様we used beforeを使用し、Specificationビルダーをconstruct Specifications out of each of these nodes we visitに実装します。
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;
}
}
方法に注意してください。
-
LogicalNodeはAND/ * OR *Nodeであり、複数の子があります
-
ComparisonNodeには子がなく、Selector, Operator and the Argumentsを保持します
たとえば、クエリ「name==john」の場合、次のようになります。
-
Selector:「名前」
-
Operator:“ ==”
-
Arguments:[john]
4. カスタムSpecificationを作成する
クエリを作成するときに、Specification:を使用しました
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
仕様がジェネリックを使用しており、特定のエンティティ(ユーザーなど)に関連付けられていないことに注意してください。
次へ–デフォルトの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. テストの同等性
次の例では、firstとlast nameでユーザーを検索します。
@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. テストの否定
次に、first nameで「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. 大なり記号をテストする
次へ–ageが「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」で始まるfirst nameのユーザーを検索します。
@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. テストイン
次に–first nameが「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は次のとおりです。
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のクエリ/検索言語を構築する方法を説明しました。
この記事のfull implementationは、the GitHub projectにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。