«1. Обзор
В этом руководстве мы рассмотрим создание языка запросов для REST API с использованием Spring Data JPA и Querydsl.
В первых двух статьях этой серии мы создали одну и ту же функцию поиска/фильтрации с использованием критериев JPA и спецификаций Spring Data JPA.
Итак, почему язык запросов? Потому что — для любого достаточно сложного 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:
SearchCriteria содержит детали, необходимые для представления ограничения:
public class SearchCriteria {
private String key;
private String operation;
private Object value;
}
key: имя поля — например: firstName, age, … и т. д. операция: операция — например: равенство, меньше, … и т. д. значение: значение поля — например: john, 25 , … и т.д.
-
5. MyUserRepository
Теперь давайте взглянем на наш MyUserRepository.
Нам нужен наш MyUserRepository для расширения QuerydslPredicateExecutor, чтобы мы могли использовать предикаты позже для фильтрации результатов поиска:
Обратите внимание, что здесь мы используем сгенерированный Q-тип для объекта MyUser, который будет называться QMyUser .
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);
}
}
6. Объединение предикатов
Далее — давайте рассмотрим объединение предикатов для использования нескольких ограничений при фильтрации результатов.
В следующем примере — мы работаем с компоновщиком — MyUserPredicatesBuilder — для объединения предикатов:
7. Протестируйте поисковые запросы
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;
}
}
Далее — давайте проверим наш поисковый 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<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)));
}
Теперь давайте посмотрим, как искать пользователя MyUser, которого на самом деле не существует:
@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());
}
8. UserController
@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)));
}
Наконец, давайте соберем все вместе и создадим REST API.
Мы определяем UserController, который определяет простой метод findAll() с параметром «search» для передачи в строке запроса:
Вот пример быстрого тестового URL:
@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);
}
}
~ ~~ И ответ:
http://localhost:8080/myusers?search=lastName:doe,age>25
9. Заключение
[{
"id":2,
"firstName":"tom",
"lastName":"doe",
"email":"[email protected]",
"age":26
}]
В этой третьей статье были рассмотрены первые шаги создания языка запросов для REST API с использованием библиотеки Querydsl.
Реализация, конечно, находится на ранней стадии, но ее можно легко развить для поддержки дополнительных операций.
Полную реализацию этой статьи можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.
«