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

Введение в Querydsl

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

1. Введение

Это вводная статья, которая поможет вам приступить к работе с мощным API Querydsl для сохранения данных.

Цель состоит в том, чтобы дать вам практические инструменты для добавления Querydsl в ваш проект, понять структуру и назначение сгенерированных классов и получить общее представление о том, как писать запросы к базе данных с типобезопасностью для наиболее распространенных сценариев.

2. Назначение Querydsl

Среды объектно-реляционного отображения лежат в основе Enterprise Java. Они компенсируют несоответствие между объектно-ориентированным подходом и моделью реляционной базы данных. Они также позволяют разработчикам писать более чистый и лаконичный код сохраняемости и логику предметной области.

Тем не менее, одним из самых сложных вариантов дизайна для платформы ORM является API для построения правильных и типобезопасных запросов.

Одна из наиболее широко используемых сред Java ORM, Hibernate (а также тесно связанный стандарт JPA), предлагает язык запросов на основе строк HQL (JPQL), очень похожий на SQL. Очевидными недостатками этого подхода являются отсутствие безопасности типов и статической проверки запросов. Кроме того, в более сложных случаях (например, когда запрос должен быть построен во время выполнения в зависимости от некоторых условий), построение запроса HQL обычно включает конкатенацию строк, что обычно очень небезопасно и подвержено ошибкам.

Стандарт JPA 2.0 принес улучшение в виде Criteria Query API — нового и безопасного для типов метода построения запросов, в котором использовались классы метамодели, созданные во время предварительной обработки аннотаций. К сожалению, будучи новаторским по своей сути, Criteria Query API оказался очень многословным и практически нечитаемым. Вот пример из учебника Jakarta EE для создания такого простого запроса, как SELECT p FROM Pet p :

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Pet> cq = cb.createQuery(Pet.class);
Root<Pet> pet = cq.from(Pet.class);
cq.select(pet);
TypedQuery<Pet> q = em.createQuery(cq);
List<Pet> allPets = q.getResultList();

Неудивительно, что вскоре появилась более адекватная библиотека Querydsl , основанная на той же идее генерируемых классов метаданных, но реализованная с помощью плавного и удобочитаемого API.

3. Генерация класса Querydsl

Давайте начнем с создания и исследования волшебных метаклассов, отвечающих за свободный API Querydsl.

3.1. Добавление Querydsl в Maven Build

Включить Querydsl в свой проект так же просто, как добавить несколько зависимостей в файл сборки и настроить плагин для обработки аннотаций JPA. Начнем с зависимостей. Версия библиотек Querydsl должна быть извлечена в отдельное свойство внутри раздела <project><properties> следующим образом (последнюю версию библиотек Querydsl можно найти в репозитории Maven Central ):

<properties>
<querydsl.version>4.1.3</querydsl.version>
</properties>

Затем добавьте следующие зависимости в раздел <project><dependencies> вашего файла pom.xml :

<dependencies>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>

</dependencies>

Зависимость querydsl-apt — это инструмент обработки аннотаций (APT) — реализация соответствующего Java API, которая позволяет обрабатывать аннотации в исходных файлах до того, как они перейдут на стадию компиляции. Этот инструмент генерирует так называемые Q-типы — классы, которые напрямую относятся к классам сущностей вашего приложения, но имеют префикс Q. Например, если в вашем приложении есть класс User , помеченный аннотацией @Entity , то сгенерированный Q-тип будет находиться в исходном файле QUser.java .

Предоставленная область зависимости querydsl -apt означает, что этот jar-файл должен быть доступен только во время сборки, но не должен быть включен в артефакт приложения.

Библиотека querydsl-jpa — это сам Querydsl, предназначенный для использования вместе с приложением JPA.

Чтобы настроить плагин обработки аннотаций, который использует преимущества querydsl-apt , добавьте следующую конфигурацию плагина в свой pom — внутри элемента <project><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>
</plugin>

Этот плагин гарантирует, что Q-типы генерируются во время цели процесса сборки Maven. Свойство конфигурации outputDirectory указывает на каталог, в котором будут созданы исходные файлы Q-типа. Значение этого свойства пригодится позже, когда вы будете исследовать Q-файлы.

Вы также должны добавить этот каталог в исходные папки проекта, если ваша IDE не делает этого автоматически — обратитесь к документации для вашей любимой IDE, чтобы узнать, как это сделать.

В этой статье мы будем использовать простую JPA-модель службы блогов, состоящую из пользователей и их сообщений в блогах , между которыми существует отношение «один ко многим»:

@Entity
public class User {

@Id
@GeneratedValue
private Long id;

private String login;

private Boolean disabled;

@OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
private Set<BlogPost> blogPosts = new HashSet<>(0);

// getters and setters

}

@Entity
public class BlogPost {

@Id
@GeneratedValue
private Long id;

private String title;

private String body;

@ManyToOne
private User user;

// getters and setters

}

Чтобы сгенерировать Q-типы для вашей модели, просто запустите:

mvn compile

3.2. Изучение сгенерированных классов

Теперь перейдите в каталог, указанный в свойстве outputDirectory apt-maven-plugin ( в нашем примере target/generated-sources/java ). Вы увидите структуру пакета и класса, которая напрямую отражает модель вашей предметной области, за исключением того, что все классы начинаются с буквы Q ( в нашем случае QUser и QBlogPost ).

Откройте файл QUser.java . Это ваша отправная точка для создания всех запросов, в которых пользователь является корневым объектом. Первое, что вы заметите, это аннотация @Generated , которая означает, что этот файл был создан автоматически и не должен редактироваться вручную. Если вы измените какой-либо из ваших классов модели предметной области, вам придется снова запустить компиляцию mvn , чтобы перегенерировать все соответствующие Q-типы.

Помимо нескольких конструкторов QUser , представленных в этом файле, вам также следует обратить внимание на общедоступный статический конечный экземпляр класса QUser :

public static final QUser user = new QUser("user");

Это экземпляр, который вы можете использовать в большинстве своих запросов Querydsl к этому объекту, за исключением случаев, когда вам нужно написать несколько более сложных запросов, таких как объединение нескольких разных экземпляров таблицы в одном запросе.

Последнее, что следует отметить, это то, что для каждого поля класса сущности существует соответствующее поле *Path в Q-типе, например, NumberPath id , StringPath login и SetPath blogPosts в классе QUser (обратите внимание, что имя поля соответствующий Set во множественном числе). Эти поля используются как часть API запросов Fluent, с которым мы столкнемся позже.

4. Запросы с помощью Querydsl

4.1. Простой запрос и фильтрация

Чтобы построить запрос, сначала нам понадобится экземпляр JPAQueryFactory , который является предпочтительным способом запуска процесса построения. Единственное, что нужно JPAQueryFactory , — это EntityManager , который уже должен быть доступен в вашем приложении JPA через вызов EntityManagerFactory.createEntityManager() или внедрение @PersistenceContext .

EntityManagerFactory emf = 
Persistence.createEntityManagerFactory("com.foreach.querydsl.intro");
EntityManager em = entityManagerFactory.createEntityManager();
JPAQueryFactory queryFactory = new JPAQueryFactory(em);

Теперь давайте создадим наш первый запрос:

QUser user = QUser.user;

User c = queryFactory.selectFrom(user)
.where(user.login.eq("David"))
.fetchOne();

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

Метод selectFrom JPAQueryFactory начинает построение запроса. Мы передаем ему экземпляр QUser и продолжаем строить условное предложение запроса с помощью метода .where() . user.login — это ссылка на поле StringPath класса QUser , которое мы видели ранее. Объект StringPath также имеет метод .eq() , который позволяет плавно продолжить построение запроса, указав условие равенства полей.

Наконец, чтобы извлечь значение из базы данных в контекст постоянства, мы завершаем цепочку построения вызовом метода fetchOne() . Этот метод возвращает значение null , если объект не может быть найден, но создает исключение NonUniqueResultException , если существует несколько объектов, удовлетворяющих условию .where() .

4.2. Заказ и группировка

Теперь давайте получим всех пользователей в списке, отсортированном по их логину в порядке возрастания.

List<User> c = queryFactory.selectFrom(user)
.orderBy(user.login.asc())
.fetch();

Такой синтаксис возможен, потому что классы *Path имеют методы .asc() и .desc() . Вы также можете указать несколько аргументов для метода .orderBy() для сортировки по нескольким полям.

Теперь попробуем что-нибудь посложнее. Допустим, нам нужно сгруппировать все посты по заголовку и подсчитать дублирующиеся заголовки. Это делается с помощью предложения .groupBy() . Мы также хотим упорядочить заголовки по результирующему количеству вхождений.

NumberPath<Long> count = Expressions.numberPath(Long.class, "c");

List<Tuple> userTitleCounts = queryFactory.select(
blogPost.title, blogPost.id.count().as(count))
.from(blogPost)
.groupBy(blogPost.title)
.orderBy(count.desc())
.fetch();

Мы выбрали заголовок поста в блоге и количество дубликатов, сгруппировали по заголовку, а затем упорядочили по совокупному количеству. Обратите внимание, что сначала мы создали псевдоним для поля count() в файле . select() , потому что нам нужно было сослаться на него в предложении .orderBy() .

4.3. Сложные запросы с объединениями и подзапросами

Давайте найдем всех пользователей, которые написали пост под названием «Hello World!» Для такого запроса мы могли бы использовать внутреннее соединение. Обратите внимание, что мы создали псевдоним blogPost для присоединяемой таблицы, чтобы ссылаться на нее в предложении .on() :

QBlogPost blogPost = QBlogPost.blogPost;

List<User> users = queryFactory.selectFrom(user)
.innerJoin(user.blogPosts, blogPost)
.on(blogPost.title.eq("Hello World!"))
.fetch();

Теперь попробуем добиться того же с подзапросом:

List<User> users = queryFactory.selectFrom(user)
.where(user.id.in(
JPAExpressions.select(blogPost.user.id)
.from(blogPost)
.where(blogPost.title.eq("Hello World!"))))
.fetch();

Как мы видим, подзапросы очень похожи на запросы, и они тоже вполне читабельны, но начинаются с фабричных методов JPAExpressions . Чтобы связать подзапросы с основным запросом, как всегда, мы ссылаемся на определенные и используемые ранее псевдонимы.

4.4. Изменение данных

JPAQueryFactory позволяет не только строить запросы, но также изменять и удалять записи. Сменим логин пользователя и отключим аккаунт:

queryFactory.update(user)
.where(user.login.eq("Ash"))
.set(user.login, "Ash2")
.set(user.disabled, true)
.execute();

У нас может быть любое количество предложений .set() для разных полей. Предложение .where() не обязательно, поэтому мы можем обновить все записи сразу.

Чтобы удалить записи, соответствующие определенному условию, мы можем использовать аналогичный синтаксис:

queryFactory.delete(user)
.where(user.login.eq("David"))
.execute();

Предложение .where() также не обязательно, но будьте осторожны, потому что пропуск предложения .where() приводит к удалению всех объектов определенного типа.

Вы можете задаться вопросом, почему JPAQueryFactory не имеет метода .insert() . Это ограничение интерфейса JPA Query. Базовый метод javax.persistence.Query.executeUpdate() способен выполнять операторы обновления и удаления, но не вставки. Чтобы вставить данные, вы должны просто сохранить сущности с помощью EntityManager.

Если вы по-прежнему хотите использовать аналогичный синтаксис Querydsl для вставки данных, вам следует использовать класс SQLQueryFactory , который находится в библиотеке querydsl-sql.

5. Вывод

В этой статье мы обнаружили мощный и типобезопасный API для манипулирования постоянными объектами, предоставляемый Querydsl.

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

Весь исходный код примеров можно найти в репозитории github .

Наконец, Querydsl предоставляет гораздо больше функций, включая работу с необработанным SQL, непостоянными коллекциями, базами данных NoSQL и полнотекстовым поиском, и мы рассмотрим некоторые из них в следующих статьях.