Skip to main content
Advertisement

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?

ApproachProsCons
Method namesGood for simple conditionsMethod explosion with many/OR/dynamic conditions
@Query JPQLCan express complex queriesString-based; typos and refactors are risky; dynamic conditions require string concatenation
QuerydslType-safe, IDE completion, dynamic conditions as codeSetup 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 Member and 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

MethodJPQL/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

GoalApproach
Type-safe + dynamic conditionsJPAQueryFactory + BooleanBuilder
JOIN / FETCH JOINjoin(), fetchJoin()
Paginationoffset(), limit() + count query or Pageable
DTO projectionProjections.constructor
Simple repo integrationQuerydslPredicateExecutor

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.

Advertisement