Post

QueryDSL в Spring Boot: полный разбор с примерами

QueryDSL в Spring Boot: полный разбор с примерами

Зачем нужен QueryDSL

QueryDSL — это type-safe DSL-библиотека для построения запросов в Java. Она генерирует классы, соответствующие сущностям JPA, что позволяет писать SQL-подобные запросы с автодополнением и проверкой на этапе компиляции.

Основная цель QueryDSL — устранить проблемы строковых запросов, которые часто возникают при использовании JPQL или HQL, и упростить создание динамических фильтров, сохраняя типовую безопасность кода.

Проблемы строковых запросов и преимущества type-safe подхода

Например, рассмотрим строковый запрос:

1
2
3
entityManager.createQuery("SELECT c FROM Course c WHERE c.name = :name", Course.class)
    .setParameter("name", name)
    .getResultList();

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

QueryDSL генерирует Q-классы (например, QCourse) через Annotation Processing Tool (APT) на этапе компиляции. Благодаря этому решаются вышеприведенные проблемы, а также становится доступным удобный builder API – построение сложных динамических условий и фильтров становится простым и читаемым.

Например:

1
2
3
4
5
QCourse course = QCourse.course;
List<Course> courses = queryFactory
    .selectFrom(course)
    .where(course.name.eq("Spring Boot"))
    .fetch();

Здесь при рефакторинге поля name на courseName компилятор сразу подсветит, что name больше не существует, а IDE подскажет правильное новое имя.

Генерация Q-классов

QueryDSL использует APT (Annotation Processing Tool), который при компиляции проекта генерирует специальные классы с префиксом Q для каждой аннотированной сущности JPA. Эти классы содержат метаинформацию о полях, что позволяет строить запросы в type-safe стиле.

Как работает APT

APT сканирует аннотации JPA, такие как @Entity, и генерирует класс, начинающийся с Q. Например, для Course.java будет сгенерирован QCourse.java в директории target/generated-sources.

В сгенерированном классе содержатся:

  • Поля, соответствующие колонкам таблицы (StringPath name, NumberPath<Long> id и т.д.)
  • Singleton-экземпляр для удобного обращения: public static final QCourse course = new QCourse("course");

Настройка Maven для генерации

В pom.xml необходимо указать annotationProcessorPaths в maven-compiler-plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.10.1</version>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <annotationProcessorPaths>
            <path>
                <groupId>com.querydsl</groupId>
                <artifactId>querydsl-apt</artifactId>
                <version>${querydsl.version}</version>
            </path>
            <path>
                <groupId>com.querydsl</groupId>
                <artifactId>querydsl-jpa</artifactId>
                <version>${querydsl.version}</version>
            </path>
            <path>
                <groupId>javax.persistence</groupId>
                <artifactId>javax.persistence-api</artifactId>
                <version>2.2</version>
            </path>
        </annotationProcessorPaths>
        <compilerArgs>
            <arg>-Aquerydsl.entityAccessors=true</arg>
            <arg>-Aquerydsl.createDefaultVariable=true</arg>
            <arg>-processor</arg>
            <arg>com.querydsl.apt.jpa.JPAAnnotationProcessor</arg>
        </compilerArgs>
    </configuration>
</plugin>

annotationProcessorPaths подключает необходимые процессоры аннотаций, compilerArgs включает генерацию Q-классов и их доступность в IDE.

После сборки проекта (mvn clean compile) в target/generated-sources появятся Q-классы.

⚠️ В IDE может потребоваться добавить эту папку в Source Root, чтобы классы были доступны для автодополнения.

Нюансы апгрейда до Java 21 и Spring Boot 3

При работе с QueryDSL важно учитывать совместимость версий Java и Spring Boot:

  • Spring Boot 2.7.x + Java 17 (рассматриваемая здесь конфигурация). QueryDSL официально поддерживает javax.persistence, что полностью совместимо с Java 17 и Spring Boot 2.7.x. Генерация Q-классов через APT происходит без ошибок, при условии корректной настройки annotationProcessorPaths в Maven.
  • Spring Boot 3 + Java 21. При переходе на Spring Boot 3 меняется namespace persistence: вместо javax.persistence используется jakarta.persistence. Это приводит к ошибке компиляции: java.lang.NoClassDefFoundError: javax/persistence/Entity. Причина в том, что текущие стабильные версии QueryDSL ожидают javax.persistence.Entity при генерации. Для решения можно использовать последние snapshot-версии QueryDSL с поддержкой Jakarta Persistence (но они могут быть нестабильны), либо форки QueryDSL, пересобранные под jakarta.persistence, опубликованные например через GitHub Packages.

Если используется fork QueryDSL, например для Jakarta-совместимости, его публикация в GitHub Packages позволяет подключить зависимость напрямую через:

1
2
3
4
5
6
<repositories>
  <repository>
    <id>github</id>
    <url>https://maven.pkg.github.com/OWNER/REPO</url>
  </repository>
</repositories>

В pom.xml также потребуется указание <pluginRepositories> для использования форка в плагинах Maven.

Таким образом, APT не имеет альтернативы как механизм генерации, но важно выбирать совместимые версии библиотек, либо использовать fork, если проект переходит на Spring Boot 3 + Jakarta Persistence.

Почему QueryDSL предпочтительнее Criteria API

QueryDSLCriteria API
Простой, декларативный DSLСложный и многословный builder-style
Полная type-safe проверкаType-safe, но крайне громоздкий синтаксис
Легко читается и поддерживаетсяТрудно читаем и поддерживается с трудом

Пример Criteria API (для сравнения):

1
2
3
4
5
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Course> query = cb.createQuery(Course.class);
Root<Course> root = query.from(Course.class);
query.select(root).where(cb.equal(root.get("name"), "Java Programming"));
List<Course> result = em.createQuery(query).getResultList();

Та же логика с QueryDSL:

1
2
3
4
5
QCourse course = QCourse.course;
List<Course> result = queryFactory
        .selectFrom(course)
        .where(course.name.eq("Java Programming"))
        .fetch();

Основные подходы работы с QueryDSL

QuerydslPredicateExecutor в репозиториях

Один из самых простых способов интеграции QueryDSL в проект — это расширить JpaRepository интерфейсом QuerydslPredicateExecutor. При таком подходе Spring Data автоматически генерирует методы для выполнения запросов с использованием предикатов QueryDSL. Это работает за счет того, что QuerydslPredicateExecutor является частью Spring Data, и при старте приложения Spring создает прокси-реализацию этого интерфейса, добавляя поддержку QueryDSL на лету без необходимости вручную писать реализацию.

Например, репозиторий может выглядеть так:

1
2
3
4
public interface CourseRepository
        extends JpaRepository<Course, Long>,
        QuerydslPredicateExecutor<Course> {
}

После этого в сервисном слое можно использовать QueryDSL-предикаты для построения фильтров. Например, чтобы получить все курсы, содержащие в названии подстроку "Java" (без учета регистра), достаточно написать:

1
2
3
QCourse course = QCourse.course;
Predicate predicate = course.name.containsIgnoreCase("Java");
Iterable<Course> results = courseRepository.findAll(predicate);

В этом примере создается Predicate на базе сгенерированного Q-класса QCourse, и метод findAll возвращает все записи, удовлетворяющие условию.

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

Однако у него есть и ограничения. Такой способ подходит только для относительно простых запросов без сложных join’ов, подзапросов или проекций DTO. В случаях, когда требуется полная гибкость QueryDSL, обычно используют JPAQueryFactory с кастомными репозиториями.

JPAQueryFactory в кастомных репозиториях

Более гибкий и промышленный подход к использованию QueryDSL в Spring Boot-проектах — это работа через JPAQueryFactory. Такой вариант позволяет реализовывать любые сложные запросы, включая подзапросы, join’ы и проекции DTO, сохраняя при этом type-safe подход QueryDSL.

Обычно JPAQueryFactory конфигурируется как Spring Bean. Для этого создается конфигурационный класс, где бины создаются на основе EntityManager:

1
2
3
4
5
6
7
8
@Configuration
public class QuerydslConfig {

    @Bean
    public JPAQueryFactory queryFactory(EntityManager em) {
        return new JPAQueryFactory(em);
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Repository
public class CourseRepositoryCustomImpl implements ICourseRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public CourseRepositoryCustomImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public List<Course> findByNamePart(String namePart) {
        QCourse course = QCourse.course;
        return queryFactory.selectFrom(course)
                .where(course.name.containsIgnoreCase(namePart))
                .fetch();
    }
}

Интерфейс кастомного репозитория при этом выглядит так:

1
2
3
public interface ICourseRepositoryCustom {
    List<Course> findByNamePart(String namePart);
}

⚠️ Важно учитывать специфику нейминга Spring Data JPA для кастомных репозиториев: имя реализации должно совпадать с именем интерфейса плюс суффикс Impl. Например, если интерфейс называется ICourseRepositoryCustom, то его реализация должна быть названа CourseRepositoryCustomImpl. Именно по этому соглашению Spring Data при старте приложения связывает интерфейсы кастомных репозиториев с их реализациями.

Затем основной репозиторий объединяет базовый JpaRepository, QuerydslPredicateExecutor (при необходимости) и кастомный интерфейс:

1
2
3
4
5
public interface CourseRepository
        extends JpaRepository<Course, Long>,
        QuerydslPredicateExecutor<Course>,
        ICourseRepositoryCustom {
}

Этот подход обеспечивает полный контроль над SQL, создаваемым QueryDSL, и позволяет писать производительные запросы без необходимости вручную конструировать их через EntityManager и JPQL. Он особенно удобен при вынесении сложной выборки в слой репозиториев, чтобы не перегружать сервисы лишней логикой построения запросов.

Однако, в отличие от использования только QuerydslPredicateExecutor, такой подход требует создания отдельного интерфейса и его реализации. Это чуть более громоздкая настройка, но при этом именно она является промышленным стандартом при работе с QueryDSL в Spring Boot-проектах.

Альтернатива: EntityManager c JPQL и нативными запросами

Хотя QueryDSL обеспечивает type-safe построение запросов и автодополнение в IDE, в Spring Data JPA традиционно применяются подходы на базе EntityManager c JPQL или нативными SQL-запросами.

Например, JPQL-запрос через EntityManager:

1
2
3
4
5
6
7
8
9
10
11
@PersistenceContext
private EntityManager em;

public List<Course> findByNamePart(String namePart) {
    return em.createQuery(
            "SELECT c FROM Course c " +
            "WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :namePart, '%'))",
            Course.class)
        .setParameter("namePart", namePart)
        .getResultList();
}

В этом примере используется createQuery с JPQL (Java Persistence Query Language), который работает на уровне сущностей и позволяет писать кросс-базовые запросы без привязки к диалекту SQL.

Если же необходимо использовать нативный SQL-запрос, например при работе со сложной бизнес-логикой, оконными функциями или специфическим SQL-синтаксисом, можно использовать createNativeQuery:

1
2
3
4
5
6
7
8
9
10
11
@PersistenceContext
private EntityManager em;

public List<Course> findByNamePartNative(String namePart) {
    return em.createNativeQuery(
            "SELECT * FROM course " +
            "WHERE LOWER(name) LIKE LOWER(CONCAT('%', :namePart, '%'))",
            Course.class)
        .setParameter("namePart", namePart)
        .getResultList();
}

В данном случае в запросе указывается имя таблицы и ее колонки напрямую, как в обычном SQL-запросе. Вторым параметром передается класс сущности, чтобы результат автоматически маппился в Course.

Аналогичный запрос можно определить через Spring Data аннотацию @Query(nativeQuery = true):

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface CourseRepository
        extends JpaRepository<Course, Long> {

    @Query(
        value = """
            SELECT *
            FROM course
            WHERE LOWER(name) LIKE LOWER(CONCAT('%', :namePart, '%'))
            """,
        nativeQuery = true
    )
    List<Course> findByNamePart(@Param("namePart") String namePart);
}

Такие подходы хорошо работают для специфических SQL и прямого использования СУБД-возможностей. Однако по сравнению с QueryDSL они:

  • Не являются type-safe (ошибки только на этапе выполнения)
  • Не поддерживают автодополнение полей сущностей в IDE
  • Более подвержены ошибкам при рефакторинге

Поэтому в production-проектах QueryDSL предпочтителен для сложных и динамических выборок, тогда как EntityManager c native SQL применяют точечно для оптимизации и использования диалектных возможностей БД.

Сравнение подходов

ПодходПреимуществаНедостатки
QuerydslPredicateExecutorБыстрая интеграция,
минимум кода
Ограничен для сложных
запросов и join-ов
JPAQueryFactoryПолный контроль,
type-safe запросы
Требует настройки,
генерации Q-классов и кастомных репозиториев
EntityManager + JPQL/SQLПолная свобода,
поддержка native SQL
Нет type-safe,
высокая вероятность ошибок,
слабая IDE поддержка

Когда какой подход использовать

  • Для простых фильтров и быстрого прототипирования: QuerydslPredicateExecutor.
  • Для production и сложных запросов с множеством join’ов и динамических фильтров: JPAQueryFactory в кастомном репозитории.
  • Для нестандартных SQL-возможностей, которые невозможно выразить в QueryDSL: EntityManager + native queries.

Пример на GitHub

Все рассмотренные примеры, включая настройку Maven, генерацию Q-классов, конфигурацию JPAQueryFactory и тесты с Testcontainers и Flyway, собраны в проекте pets.querydsl-demo на GitHub.

This post is licensed under CC BY 4.0 by the author.