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

Язык запросов REST с Spring Data JPA и Querydsl

Как встроить функции поиска/фильтрации в API REST с помощью Spring и Querydsl.

Автор оригинала: Eugen Paraschiv.

1. Обзор

В этом уроке мы рассмотрим создание языка запросов для REST API с использованием Spring Data JPA и Querydsl .

В первых двух статьях этой серии мы создали ту же функцию поиска/фильтрации , используя критерии JPA и спецификации JPA Spring Data.

Итак – почему язык запросов? Потому что – для любого достаточно сложного API – поиска/фильтрации ваших ресурсов по очень простым полям просто недостаточно. Язык запросов является более гибким и позволяет отфильтровать именно те ресурсы , которые вам нужны.

2. Конфигурация Querydsl

Во – первых, давайте посмотрим, как настроить наш проект для использования Querydsl.

Нам нужно добавить следующие зависимости в pom.xml :

 
    com.querydsl 
    querydsl-apt 
    4.2.2
    
 
    com.querydsl 
    querydsl-jpa 
    4.2.2 

Нам также необходимо настроить плагин APT – Annotation processing tool следующим образом:


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.mysema.query.apt.jpa.JPAAnnotationProcessor
            
        
    

Это создаст Q-типы для наших сущностей.

3. Сущность “Мой пользователь”

Далее – давайте взглянем на сущность ” MyUser “, которую мы собираемся использовать в нашем поисковом API:

@Entity
public class MyUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;

    private int age;
}

4. Пользовательский Предикат С Помощью PathBuilder

Теперь – давайте создадим пользовательский Предикат на основе некоторых произвольных ограничений.

Мы используем PathBuilder здесь вместо автоматически сгенерированных Q-типов, потому что нам нужно динамически создавать пути для более абстрактного использования:

public class MyUserPredicate {

    private SearchCriteria criteria;

    public BooleanExpression getPredicate() {
        PathBuilder entityPath = new PathBuilder<>(MyUser.class, "user");

        if (isNumeric(criteria.getValue().toString())) {
            NumberPath path = entityPath.getNumber(criteria.getKey(), Integer.class);
            int value = Integer.parseInt(criteria.getValue().toString());
            switch (criteria.getOperation()) {
                case ":":
                    return path.eq(value);
                case ">":
                    return path.goe(value);
                case "<":
                    return path.loe(value);
            }
        } 
        else {
            StringPath path = entityPath.getString(criteria.getKey());
            if (criteria.getOperation().equalsIgnoreCase(":")) {
                return path.containsIgnoreCase(criteria.getValue().toString());
            }
        }
        return null;
    }
}

Обратите внимание, как реализация предиката в общем случае имеет дело с несколькими типами операций . Это связано с тем, что язык запросов по определению является открытым языком, где вы потенциально можете фильтровать по любому полю, используя любую поддерживаемую операцию.

Для представления такого рода открытых критериев фильтрации мы используем простую, но довольно гибкую реализацию – SearchCriteria :

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

Критерии поиска содержат сведения, необходимые для представления ограничения:

  • ключ : имя поля – например: Имя , возраст , … и т. д
  • операция : операция – например: Равенство, меньше, … и т. Д
  • значение : значение поля – например: джон, 25, … и т. Д

5. Моя учетная запись пользователя

Теперь – давайте взглянем на нашу MyUserRepository .

Нам нужна наша Моя учетная запись пользователя для расширения QueryDslPredicateExecutor , чтобы мы могли использовать Предикаты позже для фильтрации результатов поиска:

public interface MyUserRepository extends JpaRepository, 
  QuerydslPredicateExecutor, QuerydslBinderCustomizer {
    @Override
    default public void customize(
      QuerydslBindings bindings, QMyUser root) {
        bindings.bind(String.class)
          .first((SingleValueBinding) StringExpression::containsIgnoreCase);
        bindings.excluding(root.email);
      }
}

Обратите внимание, что здесь мы используем сгенерированный Q-тип для сущности Мой пользователь , которая будет называться Мой пользователь.

6. Комбинируйте Предикаты

Далее– давайте рассмотрим объединение предикатов для использования нескольких ограничений при фильтрации результатов.

В следующем примере – мы работаем со строителем – MyUserPredicatesBuilder – для объединения Предикатов :

public class MyUserPredicatesBuilder {
    private List params;

    public MyUserPredicatesBuilder() {
        params = new ArrayList<>();
    }

    public MyUserPredicatesBuilder with(
      String key, String operation, Object value) {
  
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    public BooleanExpression build() {
        if (params.size() == 0) {
            return null;
        }

        List predicates = params.stream().map(param -> {
            MyUserPredicate predicate = new MyUserPredicate(param);
            return predicate.getPredicate();
        }).filter(Objects::nonNull).collect(Collectors.toList());
        
        BooleanExpression result = Expressions.asBoolean(true).isTrue();
        for (BooleanExpression predicate : predicates) {
            result = result.and(predicate);
        }        
        return result;
    }
}

7. Проверьте поисковые запросы

Далее – давайте протестируем наш поисковый API.

Мы начнем с инициализации базы данных несколькими пользователями, чтобы они были готовы и доступны для тестирования:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@Rollback
public class JPAQuerydslIntegrationTest {

    @Autowired
    private MyUserRepository repo;

    private MyUser userJohn;
    private MyUser userTom;

    @Before
    public void init() {
        userJohn = new MyUser();
        userJohn.setFirstName("John");
        userJohn.setLastName("Doe");
        userJohn.setEmail("[email protected]");
        userJohn.setAge(22);
        repo.save(userJohn);

        userTom = new MyUser();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repo.save(userTom);
    }
}

Далее давайте посмотрим, как найти пользователей с заданной фамилией :

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, containsInAnyOrder(userJohn, userTom));
}

Теперь давайте посмотрим, как найти пользователя с указанным именем и фамилией :

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "John").with("lastName", ":", "Doe");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

Далее давайте посмотрим, как найти пользователя с указанной фамилией и минимальным возрастом

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("lastName", ":", "Doe").with("age", ">", "25");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userTom));
    assertThat(results, not(contains(userJohn)));
}

Теперь давайте посмотрим, как искать Моего пользователя , которого на самом деле не существует :

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder()
      .with("firstName", ":", "Adam").with("lastName", ":", "Fox");

    Iterable results = repo.findAll(builder.build());
    assertThat(results, emptyIterable());
}

Наконец – давайте посмотрим, как найти Моего пользователя с учетом только части имени – как в следующем примере:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo");

    Iterable results = repo.findAll(builder.build());

    assertThat(results, contains(userJohn));
    assertThat(results, not(contains(userTom)));
}

8. Пользовательский контроллер

Наконец, давайте соберем все вместе и создадим REST API.

Мы определяем UserController , который определяет простой метод findAll() с параметром ” поиск ” для передачи в строке запроса:

@Controller
public class UserController {

    @Autowired
    private MyUserRepository myUserRepository;

    @RequestMapping(method = RequestMethod.GET, value = "/myusers")
    @ResponseBody
    public Iterable search(@RequestParam(value = "search") String search) {
        MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder();

        if (search != null) {
            Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
            }
        }
        BooleanExpression exp = builder.build();
        return myUserRepository.findAll(exp);
    }
}

Вот пример быстрого тестового URL-адреса:

http://localhost:8080/myusers?search=lastName:doe,age>25

И реакция:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"[email protected]",
    "age":26
}]

9. Заключение

В этой третьей статье были рассмотрены первые шаги создания языка запросов для API REST , в которых эффективно используется библиотека Querydsl.

Реализация, конечно, находится на ранней стадии, но ее можно легко развить для поддержки дополнительных операций.

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