9.7 Querydsl — Type-Safe Dynamic Queries
Querydsl is a Java library for writing type-safe queries. When method-name queries or JPQL strings become limiting for complex or dynamic conditions, Querydsl lets you express conditional logic, JOINs, and dynamic queries in code with compile-time safety.
Reference: Spring Boot 3.x, Querydsl 5.x (querydsl-jpa), Java 17+
1. Why Use Querydsl?
| Approach | Pros | Cons |
|---|---|---|
| Method names | Good for simple conditions | Method explosion with many/OR/dynamic conditions |
| @Query JPQL | Can express complex queries | String-based; typos and refactors are risky; dynamic conditions require string concatenation |
| Querydsl | Type-safe, IDE completion, dynamic conditions as code | Setup and Q-class generation required |
In practice, Querydsl is often used for list/search APIs with many optional filters or dynamically built where clauses.
2. Dependencies and Q-Class Generation (Gradle)
You need querydsl-jpa and an annotation processor to generate Q-classes (query meta-models per entity).
build.gradle
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' // JPA (Jakarta)
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
- :jakarta: For Jakarta EE (JPA 3.x). Required with Spring Boot 3 / JPA 3.
- After compilation, QMember, QOrder, etc. are generated under
build/generated/sources/annotationProcessor(or your configured path).
Q-class example
For entity Member, QMember.member is generated; fields are referenced as QMember.member.name, QMember.member.id, and so on.
QMember member = QMember.member;
member.name.eq("John"); // name = 'John'
member.id.eq(1L); // id = 1
3. Injecting JPAQueryFactory
Register JPAQueryFactory as a bean and inject it in your repository or service.
@Configuration
public class QuerydslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
@Service
@RequiredArgsConstructor
public class MemberSearchService {
private final JPAQueryFactory queryFactory;
}
4. Basic Queries (select, where)
QMember member = QMember.member;
List<Member> list = queryFactory
.selectFrom(member)
.where(
member.name.eq("John"),
member.status.eq(MemberStatus.ACTIVE)
)
.orderBy(member.id.desc())
.fetch();
- selectFrom(member): Equivalent to
FROM Memberand select that entity. - where(...): Conditions are ANDed. Use eq, ne, like, in, isNotNull, etc.
- fetch(): Returns a list. Use fetchOne() for a single result, fetchFirst() for existence checks.
Common condition methods
| Method | JPQL/SQL |
|---|---|
| eq(value) | = ? |
| ne(value) | <> ? |
| like("%" + v + "%") | LIKE '%v%' |
| in(collection) | IN (?, ?, ?) |
| isNull() / isNotNull() | IS NULL / IS NOT NULL |
| goe / loe (value) | >= , <= |
| between(a, b) | BETWEEN ? AND ? |
5. JOIN and FETCH JOIN
QOrder order = QOrder.order;
QMember member = QMember.member;
List<Order> list = queryFactory
.selectFrom(order)
.join(order.member, member)
.where(member.name.eq("John"))
.fetch();
- join(order.member, member): INNER JOIN. leftJoin, rightJoin are also available.
- To load the association in the same query (avoid N+1), use join().fetchJoin():
List<Order> list = queryFactory
.selectFrom(order)
.join(order.member, member).fetchJoin()
.where(order.id.eq(1L))
.fetch();
6. Dynamic Queries (BooleanBuilder)
When search criteria are optional, combine conditions with BooleanBuilder.
public List<Member> search(String name, MemberStatus status, Integer minAge) {
QMember member = QMember.member;
BooleanBuilder builder = new BooleanBuilder();
if (name != null && !name.isBlank()) {
builder.and(member.name.containsIgnoreCase(name));
}
if (status != null) {
builder.and(member.status.eq(status));
}
if (minAge != null) {
builder.and(member.age.goe(minAge));
}
return queryFactory
.selectFrom(member)
.where(builder)
.orderBy(member.id.desc())
.fetch();
}
- builder.and(...): Add conditions with AND. or(...) is also available.
- containsIgnoreCase: Case-insensitive LIKE. startsWith, endsWith are also provided.
7. Pagination (offset, limit / Pageable)
Simple offset/limit
List<Member> list = queryFactory
.selectFrom(member)
.where(...)
.orderBy(member.id.desc())
.offset(10)
.limit(20)
.fetch();
With Spring Data Pageable
Build a Page by running a content query and a count query. In Querydsl 5.x, fetchCount() may be deprecated; prefer a dedicated count query.
public Page<Member> searchPage(Pageable pageable, String name) {
QMember member = QMember.member;
BooleanBuilder builder = new BooleanBuilder();
if (name != null && !name.isBlank()) {
builder.and(member.name.containsIgnoreCase(name));
}
List<Member> content = queryFactory
.selectFrom(member)
.where(builder)
.orderBy(member.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.selectFrom(member)
.where(builder)
.fetchCount(); // or e.g. select(member.id.count()).fetchOne()
return new PageImpl<>(content, pageable, total);
}
8. DTO Projection
To return DTOs instead of entities, use Projections.constructor (or the Q-class constructor).
public record MemberSummaryDto(Long id, String name, String email) {}
List<MemberSummaryDto> list = queryFactory
.select(
Projections.constructor(MemberSummaryDto.class,
member.id,
member.name,
member.email)
)
.from(member)
.where(member.status.eq(MemberStatus.ACTIVE))
.fetch();
- Projections.constructor(Class, fields...): Argument order must match the DTO constructor.
- Projections.fields() uses reflection and setter injection (field names must match).
9. Using with Spring Data JPA (QuerydslPredicateExecutor)
Extend QuerydslPredicateExecutor on your repository to use findAll(Predicate), count(Predicate), etc. Handy for simple predicate-based queries.
public interface MemberRepository extends JpaRepository<Member, Long>, QuerydslPredicateExecutor<Member> {
}
QMember member = QMember.member;
Predicate predicate = member.name.eq("John").and(member.status.eq(MemberStatus.ACTIVE));
List<Member> list = memberRepository.findAll(predicate);
- For complex dynamic queries, DTO projection, or subqueries, JPAQueryFactory is usually more flexible.
10. Summary
| Goal | Approach |
|---|---|
| Type-safe + dynamic conditions | JPAQueryFactory + BooleanBuilder |
| JOIN / FETCH JOIN | join(), fetchJoin() |
| Pagination | offset(), limit() + count query or Pageable |
| DTO projection | Projections.constructor |
| Simple repo integration | QuerydslPredicateExecutor |
Querydsl is a good fit for complex search APIs and list endpoints with many optional filters. The JOIN and N+1 concepts from 9.6 JPQL JOIN and Advanced Queries apply in Querydsl via fetchJoin() and similar.