クエリメソッドやJPQLを使った検索に慣れてきたら、次のステップがSpecificationによる動的クエリですよね。でも条件が増えてくると、ちょっと限界を感じてきませんか。そこでQueryDSLです。

Specificationの限界とQueryDSLを選ぶ理由

Specificationで条件を積み重ねるとこんなコードになります。

Specification<User> spec = Specification.where(null);
if (name != null) spec = spec.and(hasName(name));
if (status != null) spec = spec.and(hasStatus(status));
if (from != null) spec = spec.and(createdAfter(from));

コード自体は悪くないのですが、 IDE補完 が効きにくいのが難点です。フィールド名を文字列で扱うことが多く、タイポしてもコンパイルエラーになりません。条件が増えるほど保守性が下がっていきます。

QueryDSLはJavaコードでクエリを書くため、型チェックと補完が完全に機能します。リファクタリングも安心です。

QueryDSLの仕組みを30秒で理解する

QueryDSLは APT(Annotation Processing Tool) を使って、エンティティクラスから「Q型クラス」を自動生成します。

User エンティティがあれば QUser が生成され、QUser.user.name.eq("Alice") のように型安全に条件を記述できます。ビルド時に生成されるのでIDEの補完も完全に機能するわけです。

本記事では QueryDSL 5.1.0Spring Boot 3.x(Jakarta EE) を対象にします。

Mavenでの設定

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

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>com.querydsl</groupId>
                        <artifactId>querydsl-apt</artifactId>
                        <version>5.1.0</version>
                        <classifier>jakarta</classifier>
                    </path>
                    <path>
                        <groupId>jakarta.persistence</groupId>
                        <artifactId>jakarta.persistence-api</artifactId>
                        <version>3.1.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

mvn compile を実行すると target/generated-sources/annotations 以下にQ型クラスが生成されます。IntelliJ IDEAではこのディレクトリを Generated Sources Root としてマークしておきましょう。

Gradleでの設定

dependencies {
    implementation "com.querydsl:querydsl-jpa:5.1.0:jakarta"
    annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api:3.1.0"
}

sourceSets {
    main {
        java {
            srcDirs += ["build/generated/sources/annotationProcessor/java/main"]
        }
    }
}

./gradlew compileJava でQ型が生成されます。sourceSets を正しく設定すれば、Gradleプロジェクトのインポート時にIntelliJが自動認識します。認識されない場合は generated-sources ディレクトリを右クリック →「Generated Sources Root」で手動マークしてください。

JPAQueryFactoryのBean定義

QueryDSLのエントリポイントとなる JPAQueryFactory を登録します。

@Configuration
@RequiredArgsConstructor
public class QueryDslConfig {

    private final EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

Springが EntityManager にスレッドセーフなプロキシを注入するため、コンストラクタインジェクションで安全に使えます。

BooleanBuilderで動的クエリを書く

ユーザーを名前・ステータス・登録日で絞り込む例です。

@Repository
public class UserQueryRepository {

    private final JPAQueryFactory queryFactory;

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

    public List<User> searchUsers(String name, UserStatus status, LocalDate from) {
        QUser user = QUser.user;
        BooleanBuilder builder = new BooleanBuilder();

        if (name != null) builder.and(user.name.containsIgnoreCase(name));
        if (status != null) builder.and(user.status.eq(status));
        if (from != null) builder.and(user.createdAt.goe(from));

        return queryFactory
                .selectFrom(user)
                .where(builder)
                .orderBy(user.createdAt.desc())
                .fetch();
    }
}

null のパラメーターはスキップされるので、そのまま動的クエリになります。Specificationと比べてコードの意図が読みやすいですよね。

BooleanExpressionでメソッド分割する

条件をstaticメソッドに切り出すと再利用しやすくなります。

private BooleanExpression nameContains(QUser user, String name) {
    return name != null ? user.name.containsIgnoreCase(name) : null;
}

private BooleanExpression statusEq(QUser user, UserStatus status) {
    return status != null ? user.status.eq(status) : null;
}

// 使う側
queryFactory
    .selectFrom(user)
    .where(nameContains(user, name), statusEq(user, status))
    .fetch();

where()null を渡すと条件がスキップされます。各条件を独立してユニットテストできるのも利点です。条件を他のクエリと共有したい場合や、条件が5つを超えてきた場合はBooleanExpressionに切り出すと保守しやすくなります。

Pageableとのページネーション

Spring Data JPAの Pageable と組み合わせる場合はこうなります。

public Page<User> searchUsersPage(String name, Pageable pageable) {
    QUser user = QUser.user;
    BooleanExpression condition = nameContains(user, name);

    List<User> content = queryFactory
            .selectFrom(user)
            .where(condition)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    Long total = Objects.requireNonNullElse(
        queryFactory.select(user.count()).from(user).where(condition).fetchOne(),
        0L
    );

    return new PageImpl<>(content, pageable, total);
}

QueryDSL 5.x では fetchResults()fetchCount() がいずれも非推奨になりました。件数取得は select(user.count()).fetchOne() で代替するのが現在の推奨パターンです。fetchOne() が返す Long はnullになりうるため、Objects.requireNonNullElse でnull安全に扱っています。

ページネーション全体の設計については ページネーション実装ガイド も参考にどうぞ。

QuerydslPredicateExecutorとの使い分け

すでに QuerydslPredicateExecutor<T> をリポジトリに継承している場合、シンプルな条件ならそのまま使い続けられます。ただし複合条件のカスタマイズや細かいページネーション制御には限界があります。複合条件が必要になってきたタイミングで、今回のカスタムリポジトリパターンへ段階的に移行するのが現実的な選択肢です。

SpecificationとQueryDSLの比較

観点SpecificationQueryDSL
セットアップ不要APT設定が必要
IDE補完限定的完全に機能
可読性条件が増えると低下高い
複雑条件辛い得意
テスト容易性やや難条件を個別にテストしやすい
向いているケースシンプルな検索複合条件・再利用

Specificationパターン は条件が3〜4つ以下のシンプルな検索なら十分です。条件が複雑化してきたり、チームで再利用性を高めたいならQueryDSLが向いています。既存のSpecificationリポジトリと共存させながら段階的に移行することも可能です。

セットアップで詰まったときは

Q型クラスが生成されない 原因の多くは maven-compiler-pluginannotationProcessorPaths の設定ミスです。querydsl-apt<dependency> に追加するだけでは不十分で、プラグイン設定への追加も必要です。

<!-- ❌ dependenciesに追加するだけでは動かない -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.1.0</version>
    <classifier>jakarta</classifier>
    <scope>provided</scope>
</dependency>

<!-- ✅ maven-compiler-plugin の annotationProcessorPaths にも追加する -->
<annotationProcessorPaths>
    <path>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>5.1.0</version>
        <classifier>jakarta</classifier>
    </path>
    <path>
        <groupId>jakarta.persistence</groupId>
        <artifactId>jakarta.persistence-api</artifactId>
        <version>3.1.0</version>
    </path>
</annotationProcessorPaths>

IDEがQ型を認識しない 場合は、generated-sourcesのディレクトリがSources Rootとして設定されているか確認しましょう。

javax.persistencejakarta.persistence の混在 もよくあるトラブルです。Spring Boot 3.x系は jakarta パッケージなので、依存関係の classifierjakarta を指定するのを忘れずに。

まとめ

QueryDSLを導入すると、複雑な動的クエリもJavaコードとして読みやすく保守しやすく書けるようになります。APTのセットアップに少し手間がかかりますが、一度動けば開発体験がかなり向上しますよ。まずはシンプルなリポジトリクラスから試してみてください。

パフォーマンスの最適化については Spring Data JPAパフォーマンス最適化 も参考にどうぞ。