Введение в Querydsl

Введение в Querydsl

1. Вступление

Это вводная статья, которая познакомит вас с мощным APIQuerydsl для сохранения данных.

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

2. Цель Querydsl

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

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

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

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

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

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

3. Создание класса Querydsl

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

3.1. Добавление Querydsl в сборку Maven

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


    4.1.3

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



    
        com.querydsl
        querydsl-apt
        ${querydsl.version}
        provided
    

    
        com.querydsl
        querydsl-jpa
        ${querydsl.version}
    

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

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

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

Чтобы настроить плагин обработки аннотаций, который используетquerydsl-apt, добавьте следующую конфигурацию плагина в ваш pom - внутри элемента<project><build><plugins>:


    com.mysema.maven
    apt-maven-plugin
    1.1.3
    
        
            
                process
            
            
                target/generated-sources/java
                com.querydsl.apt.jpa.JPAAnnotationProcessor
            
        
    

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

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

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

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String login;

    private Boolean disabled;

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
    private Set 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. Это ваша точка входа для создания всех запросов, в которыхUser является корневым объектом. Первое, что вы заметите, это аннотация@Generated, которая означает, что этот файл был создан автоматически и не должен редактироваться вручную. Если вы измените какой-либо из классов модели предметной области, вам придется снова запуститьmvn compile, чтобы регенерировать все соответствующие Q-типы.

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

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

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

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

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

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

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

EntityManagerFactory emf =
  Persistence.createEntityManagerFactory("org.example.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 user и инициализировали ее статическим экземпляромQUser.user. Это сделано исключительно для краткости, в качестве альтернативы вы можете импортировать статическое полеQUser.user.

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

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

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

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

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

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

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

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

List 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 users = queryFactory.selectFrom(user)
  .innerJoin(user.blogPosts, blogPost)
  .on(blogPost.title.eq("Hello World!"))
  .fetch();

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

List 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 repository.

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