Рубрики
Без рубрики

Язык запросов REST с SQL

Узнайте, как реализовать Язык поиска/запросов для API Spring REST, используя стандартный язык запросов элементов ленты вместо пользовательского синтаксиса.

Автор оригинала: 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 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 , которые мы использовали раньше – и мы собираемся реализовать Спецификацию 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 ” – у нас есть:

  1. Селектор : “имя”
  2. Оператор : “==”
  3. Аргументы :[Джон]

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
}

Обратите внимание, что спецификация использует универсальные методы и не привязана к какому-либо конкретному объекту (например, пользователю).

Далее – вот наш 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");
    Specification spec = 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");
    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. Тест В

Далее – мы будем искать пользователей по их имени “|/джон ” или “ джек “:

@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.

полную реализацию этой статьи можно найти в проекте GitHub – это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.