«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 должна быть извлечена в отдельное свойство внутри раздела \u003cproject\u003e\u003cproperties\u003e следующим образом (последнюю версию библиотек Querydsl можно найти в репозитории Maven Central):

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

Затем добавьте следующие зависимости в разделе \u003cproject\u003e\u003cdependencies\u003e вашего файла 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 — внутри элемента \u003cproject\u003e\u003cbuild\u003e\u003cplugins\u003e:

<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, логин StringPath и запись блога SetPath в классе QUser (обратите внимание, что имя поля, соответствующего Set, во множественном числе). Эти поля используются как часть API запросов Fluent, с которым мы столкнемся позже.

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

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

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

EntityManagerFactory emf = 
  Persistence.createEntityManagerFactory("com.baeldung.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. Сложные запросы с объединениями и подзапросами

QBlogPost blogPost = QBlogPost.blogPost;

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

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

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. Изменение данных

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

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

У нас может быть любое количество предложений .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.