Skip to content
Spring Boot sb data-jpa 3 min read

QueryDSL

QueryDSL is a fluent, typesafe query API. Instead of building queries from strings (JPQL) or verbose CriteriaBuilder calls, you write predicates against generated metamodel classes — the Q-classes — so column names, types, and operators are checked by the compiler. Rename a field and your queries stop compiling rather than failing at runtime.

It plugs directly into Spring Data through QuerydslPredicateExecutor, making it an ergonomic alternative to Specifications for dynamic, runtime-composed filters.

Dependencies and the APT processor

QueryDSL ships a separate annotation processor that scans your @Entity classes and generates a Q<Entity> companion for each one. Add the JPA artifact (Jakarta classifier on Spring Boot 3) plus the processor:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <classifier>jakarta</classifier>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <classifier>jakarta</classifier>
    <scope>provided</scope>
</dependency>

Wire the processor into the Maven compile phase so Q-classes are generated into target/generated-sources:

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals><goal>process</goal></goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

Tip: Run mvn compile (or your IDE’s build) once after adding the entity. Until the Q-classes are generated, QProduct will not exist and your editor will flag it as missing.

For an entity:

@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String category;
    private BigDecimal price;
    // getters/setters
}

…the processor emits QProduct with a typed field per attribute: QProduct.product.name, QProduct.product.price, and so on.

QuerydslPredicateExecutor on the repository

Extend QuerydslPredicateExecutor<T> alongside JpaRepository:

public interface ProductRepository
        extends JpaRepository<Product, Long>, QuerydslPredicateExecutor<Product> {
}

This adds predicate-driven methods to the repository:

Optional<T>   findOne(Predicate predicate);
Iterable<T>   findAll(Predicate predicate);
Page<T>       findAll(Predicate predicate, Pageable pageable);
Iterable<T>   findAll(Predicate predicate, Sort sort);
long          count(Predicate predicate);
boolean       exists(Predicate predicate);

Building BooleanExpression predicates

BooleanExpression is the typesafe predicate type. The fluent operators (eq, loe, containsIgnoreCase, and, or) read like the filter itself:

QProduct product = QProduct.product;

BooleanExpression byCategory = product.category.eq("books");
BooleanExpression cheap      = product.price.loe(new BigDecimal("50"));

List<Product> result = (List<Product>) repository.findAll(byCategory.and(cheap));

The real strength is conditional composition — append a clause only when its filter is supplied, with no string concatenation:

public record ProductFilter(String category, BigDecimal maxPrice, String name) {}

@Service
public class ProductSearchService {

    private final ProductRepository repository;

    public ProductSearchService(ProductRepository repository) {
        this.repository = repository;
    }

    public Page<Product> search(ProductFilter filter, Pageable pageable) {
        QProduct product = QProduct.product;
        BooleanBuilder where = new BooleanBuilder();

        if (filter.category() != null) {
            where.and(product.category.eq(filter.category()));
        }
        if (filter.maxPrice() != null) {
            where.and(product.price.loe(filter.maxPrice()));
        }
        if (filter.name() != null && !filter.name().isBlank()) {
            where.and(product.name.containsIgnoreCase(filter.name()));
        }
        return repository.findAll(where, pageable);
    }
}

BooleanBuilder is a mutable accumulator; an empty builder matches everything, so an empty filter returns all rows (subject to paging). For a category + max-price filter the generated SQL contains only the active predicates:

select p1_0.id, p1_0.category, p1_0.name, p1_0.price
from product p1_0
where p1_0.category = ? and p1_0.price <= ?
order by p1_0.name asc
offset ? rows fetch first ? rows only

QueryDSL vs Specifications / Criteria API

All three build dynamic queries, but they differ sharply in type safety and ergonomics:

AspectQueryDSLJPA SpecificationsCriteria API (raw)
Field referencesGenerated Q-classes (compile-checked)root.get("name") stringsroot.get("name") strings
ReadabilityFluent, query-likeModerate (lambda fragments)Verbose, ceremony-heavy
Build stepRequires APT codegenNoneNone
Reusable fragmentsBooleanExpression constantsSpecification factoriesManual
Refactor safetyHigh — rename breaks compileLow — strings break at runtimeLow
Extra dependencyYes (querydsl-jpa)No (Spring Data built-in)No (JPA built-in)

Note: Specifications can use the JPA static metamodel (Product_.price) for some compile-time safety, but QueryDSL’s generated API is more complete and the fluent syntax is generally easier to read for complex predicates.

Tip: Choose QueryDSL when you have many dynamic search screens and value readable, refactor-safe queries enough to accept the codegen step. Stick with Specifications when you want zero extra build configuration.

Pitfalls

  • Forgotten codegen: if QProduct won’t resolve, the APT processor hasn’t run — rebuild and ensure target/generated-sources/java is a source root.
  • Joins and distinct: for @OneToMany joins use JPAQuery directly with .distinct() to avoid duplicate roots; the simple predicate executor doesn’t expose that knob.
  • Beyond predicates: QuerydslPredicateExecutor only filters/sorts/pages. For projections, group-by, or aggregates, inject a JPAQueryFactory and write the full query.
Last updated June 13, 2026
Was this helpful?