REST Query Language с RSQL
1. обзор
В этой пятой статьеthe series мы проиллюстрируем создание языка запросов REST API с помощьюa cool library – rsql-parser..
RSQL - это расширенный набор языка запросов элементов ленты (FIQL) - чистый и простой синтаксис фильтра для лент; поэтому он вполне естественно вписывается в REST API. **
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 JPAwe 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: [джон]
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
Обратите внимание на то, что в спецификации используются универсальные типы и не привязаны к какой-либо конкретной сущности (например, пользователю).
Далее - вот нашenum “RsqlSearchOperation“, который содержит операторы rsql-parser по умолчанию:
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. Проверить лайк
Далее - мы будем искать пользователей, у которыхfirst name начинается с «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
Далее - мы будем искать пользователей, у которых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. Заключение
В этом руководстве показано, как создать язык запросов / поиска для REST API без необходимости заново изобретать синтаксис и вместо этого использовать FIQL / RSQL.
full implementation в этой статье можно найти вthe GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.