Автор оригинала: Eugen Paraschiv.
1. Обзор
В этой пятой статье серии мы проиллюстрируем построение языка запросов REST API с помощью классной библиотеки – rsql-парсера .
RSQL-это супер – набор языка запросов элементов фида ( FIQL ) – чистый и простой синтаксис фильтра для фидов; поэтому он вполне естественно вписывается в REST API.
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. Проанализируйте запрос
Способ внутреннего представления выражений SQL представлен в виде узлов, и шаблон посетителя используется для анализа входных данных.
Имея это в виду, мы собираемся реализовать Sqlvisitor интерфейс и создать нашу собственную реализацию посетителя – CustomRsqlVisitor :
public class CustomRsqlVisitorimplements 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 , которые мы использовали раньше – и мы собираемся реализовать Спецификацию builder для построения спецификаций из каждого из этих узлов, которые мы посещаем :
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; } }
Обратите внимание, как:
- Логический узел является И / ИЛИ узлом и имеет несколько дочерних узлов
- Узел сравнения не имеет дочерних элементов и содержит Селектор , Оператор и аргументы
Например, для запроса ” name==john ” – у нас есть:
- Селектор : “имя”
- Оператор : “==”
- Аргументы :[Джон]
4. Создайте Пользовательскую Спецификацию
При построении запроса мы использовали спецификацию :
public class GenericRsqlSpecificationimplements Specification { private String property; private ComparisonOperator operator; private List arguments; @Override public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder) { List
Обратите внимание, что спецификация использует универсальные методы и не привязана к какому-либо конкретному объекту (например, пользователю).
Далее – вот наш enum “ RsqlSearchOperation “ , который содержит операторы rsql-парсера по умолчанию:
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"); Specificationspec = rootNode.accept(new CustomRsqlVisitor ()); List 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"); Specificationspec = 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"); Specificationspec = 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*"); Specificationspec = rootNode.accept(new CustomRsqlVisitor ()); List results = repository.findAll(spec); assertThat(userJohn, isIn(results)); assertThat(userTom, not(isIn(results))); }
5.5. Тест В
Далее – мы будем искать пользователей по их имени “|/джон ” или “ джек “:
@Test public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); Specificationspec = 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 ListfindAllByRsql(@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.
полную реализацию этой статьи можно найти в проекте GitHub – это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.