В статье, посвящённой автоматической генерации запросов в Spring Data Commons, я упоминал о двух проблемах такого подхода: статичность запросов и одновременное отсутствие типобезопасности. Одно из решений этой проблемы — библиотека Querydsl, которая позволяет строить запросы к данным (кстати не только JPA запросы) используя java в качестве языка описания запросов.
Подготовка
Querydsl своей реализацией отчасти похож на JPA Metamodel. Сущности, определённые в проекте, сканируются и для них создаются вспомогательные классы. Поэтому, помимо добавления артефакта Querydsl в зависимости, требуется так же добавить и плагин для генерации вспомогательных классов.
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <querydsl.version>4.1.4</querydsl.version> </properties> <dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency> </dependencies>
<build> <plugins> <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.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> </dependency> </dependencies> </plugin> </plugins> </build>
Разумеется, надо не забыть добавить в зависимости Spring Data JPA, сам Spring и какую-нибудь реализацию JPA.
Интерфейс QueryDslPredicateExecutor
Чтобы включить поддержку Querydsl в генерируемый репозиторий, необходимо чтоб он имел в списке предков интерфейс QueryDslPredicateExecuter<T>, где T — тип сущности, с которой работает репозиторий. При этом, даже если планируется работать только с Querydsl, наличие в предках интерфейса Repository или любого из его наследников всё равно является обязательным.
public interface PassportRepository extends CrudRepository<Passport, Long>, QueryDslPredicateExecutor<Passport> { }
public interface PersonRepository extends CrudRepository<Person, Long>, QueryDslPredicateExecutor<Person> { }
Интерфейс QueryDslPredicateExecutor<T> определяет несколько методов:
- T FindOne(Predicate) — возвращает один объект, соответствующий условия
- Iterable<T> findAll(Predicate) — возвращает несколько объектов, соответствующих условию. Обратите внимание, что возвращается всегда Iterable<T>, без возможности уточнить тип
- long count(Predicate) — возвращает количество объектов в базе данных, соответствующих условию
- boolean exists(Predicate) — сообщает, есть ли в базе данных объект соответствующий условию
Все методы принимают условие Querydsl.
Предикаты в Querydsl
Написание запросов в Querydsl настолько просто, что достаточно показать пример и сразу всё станет ясно:
QPerson qPerson = QPerson.person; Predicate personTestQuery = qPerson.firstName.eq("Test"); personRepository.findAll(personTestQuery) .forEach(System.out::println);
QPerson и QPassport выше — специальные вспомогательные типы Querydsl, которые были сгенерированы из описаний сущностей (а сами сущности я взял из вводной статьи о Spring Data Commons). Querydsl плагин автоматически создаёт классы с префиксом Q для каждого найденного им класса сущности. Внутри такого Q* класса содержится единственный глобальный объект этого класса, на основе которого уже и делаются запросы. У объекта Q* класса есть все публичные поля, которые есть у сущности, из которой он был сгенерирован. Но, в этих полях не содержится значений, у них есть методы, описывающие, какими должны быть значения поля. В примере выше я строю запрос, который требует, чтобы поле firstName сущности Person имело значение «Test».
Благодаря использованию вспомогательных классов запросы, написанные с помощью Querydsl, типобезопасны и проверяются на стадии компиляции. А создание запросов методом вызова java функций позволяет создавать динамические запросы во время исполнения.
Возможных условий в Querydsl великое множество и все они перечислены в документации. Я лишь отмечу, что их достаточно для любого практического применения:
assertFalse(personRepository.exists(qPerson.firstName.endsWith("fail")));
Обращение к полям классов, на которые ссылается сущность, реализуется весьма интуитивно:
QPassport qPassport = QPassport.passport; Predicate passportPersonQuery = qPassport.owner.lastName.startsWith("Te"); passportRepository.findAll(passportPersonQuery) .forEach(System.out::println);
Кроме того, условия можно комбинировать, использовать объединения и так далее:
Predicate passportPersonQuery = qPassport.owner.lastName.startsWith("Te").and(qPassport.series.contains("A"));
Метод and(), равно как методы or(), any() и другие, принимает в себя любой объект типа Predicate, что позволяет изменять части запроса во время исполнения.