RSQL을 사용한 REST 쿼리 언어

REST 상단

방금 Spring 5 및 Spring Boot 2의 기본 사항에 초점을 맞춘 새로운 Learn Spring 과정을 발표했습니다 .

>> 코스 Persistence top을 확인하세요

방금 Spring 5 및 Spring Boot 2의 기본 사항에 초점을 맞춘 새로운 Learn Spring 과정을 발표했습니다 .

>> 과정 확인 이 기사는 시리즈의 일부입니다. • Spring 및 JPA 기준을 사용하는 REST 쿼리 언어

• SpringData JPA 사양을 사용한 REST 쿼리 언어

• SpringData JPA 및 Querydsl을 사용한 REST 쿼리 언어

• REST 쿼리 언어 – 고급 검색 작업

• REST 쿼리 언어 – OR 운영 구현

• RSQL을 사용한 REST 쿼리 언어 (현재 기사) • Querydsl 웹 지원을 사용한 REST 쿼리 언어

1. 개요

이 시리즈의 다섯 번째 기사에서는 멋진 라이브러리 인 rsql-parser를 사용 하여 REST API 쿼리 언어를 빌드하는 방법을 설명합니다 .

RSQL은 피드에 대한 깔끔하고 간단한 필터 구문 인 FIQL (Feed Item Query Language)의 상위 집합입니다. 따라서 REST API에 아주 자연스럽게 맞습니다.

2. 준비

먼저 라이브러리에 Maven 종속성을 추가해 보겠습니다.

 cz.jirutka.rsql rsql-parser 2.1.0 

또한 예제 전체에서 작업 할 주요 엔터티를 정의합니다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 인터페이스 를 구현 하고 자체 방문자 구현 인 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. 테스트 IN

다음으로 사용자의 이름 이 " 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