Автор оригинала: 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() { PathBuilderentityPath = 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 Listparams; 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"); Iterableresults = 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"); Iterableresults = 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"); Iterableresults = 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"); Iterableresults = repo.findAll(builder.build()); assertThat(results, emptyIterable()); }
Наконец – давайте посмотрим, как найти Моего пользователя с учетом только части имени – как в следующем примере:
@Test public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() { MyUserPredicatesBuilder builder = new MyUserPredicatesBuilder().with("firstName", ":", "jo"); Iterableresults = 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 Iterablesearch(@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, поэтому его должно быть легко импортировать и запускать как есть.