クエリメソッドや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.0 と Spring 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の比較
| 観点 | Specification | QueryDSL |
|---|---|---|
| セットアップ | 不要 | APT設定が必要 |
| IDE補完 | 限定的 | 完全に機能 |
| 可読性 | 条件が増えると低下 | 高い |
| 複雑条件 | 辛い | 得意 |
| テスト容易性 | やや難 | 条件を個別にテストしやすい |
| 向いているケース | シンプルな検索 | 複合条件・再利用 |
Specificationパターン は条件が3〜4つ以下のシンプルな検索なら十分です。条件が複雑化してきたり、チームで再利用性を高めたいならQueryDSLが向いています。既存のSpecificationリポジトリと共存させながら段階的に移行することも可能です。
セットアップで詰まったときは
Q型クラスが生成されない 原因の多くは maven-compiler-plugin の annotationProcessorPaths の設定ミスです。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.persistence と jakarta.persistence の混在 もよくあるトラブルです。Spring Boot 3.x系は jakarta パッケージなので、依存関係の classifier に jakarta を指定するのを忘れずに。
まとめ
QueryDSLを導入すると、複雑な動的クエリもJavaコードとして読みやすく保守しやすく書けるようになります。APTのセットアップに少し手間がかかりますが、一度動けば開発体験がかなり向上しますよ。まずはシンプルなリポジトリクラスから試してみてください。
パフォーマンスの最適化については Spring Data JPAパフォーマンス最適化 も参考にどうぞ。