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
QueryDSL | Criteria 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.