Перейти к основному содержимому

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

· 6 мин. чтения

1. Обзор

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

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

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

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

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

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

<dependency> 
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.2.2</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.2.2</version>
</dependency>

Нам также необходимо настроить APT — инструмент обработки аннотаций — плагин следующим образом:

<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>

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

3. Сущность MyUser

Далее — давайте взглянем на объект « 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<MyUser> entityPath = new PathBuilder<>(MyUser.class, "user");

if (isNumeric(criteria.getValue().toString())) {
NumberPath<Integer> 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;
}

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

  • key : имя поля — например: firstName , age , … и т . д.
  • операция : операция – например: равенство, меньше, … и т. д.
  • value : значение поля — например: john, 25, … и т. д.

5. Мой Пользовательский Репозиторий

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

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

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

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

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

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

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

public class MyUserPredicatesBuilder {
private List<SearchCriteria> 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("john@doe.com");
userJohn.setAge(22);
repo.save(userJohn);

userTom = new MyUser();
userTom.setFirstName("Tom");
userTom.setLastName("Doe");
userTom.setEmail("tom@doe.com");
userTom.setAge(26);
repo.save(userTom);
}
}

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

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

Iterable<MyUser> 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<MyUser> 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<MyUser> results = repo.findAll(builder.build());

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

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

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

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

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

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

Iterable<MyUser> results = repo.findAll(builder.build());

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

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

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

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

@Controller
public class UserController {

@Autowired
private MyUserRepository myUserRepository;

@RequestMapping(method = RequestMethod.GET, value = "/myusers")
@ResponseBody
public Iterable<MyUser> 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":"tom@doe.com",
"age":26
}]

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

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

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

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