【Spring Boot】@Queryでカスタムクエリを書く

Java

こんにちは、かつコーチです。

前回、クエリメソッドは条件が複雑になるとメソッド名が長くなりすぎると触れました。

そんなときに使うのが@Queryアノテーションです。

今回は@Queryを使ったJPQL・ネイティブSQLの書き方を解説します。

@Queryアノテーションとは

JPQLとSQLの違い

@Queryとは、Repositoryのメソッドに直接クエリを記述できるアノテーションです。

デフォルトではJPQL(Java Persistence Query Language)という言語を使いますが、これはテーブル名ではなくEntityクラス名・フィールド名を使う点が通常のSQLと異なります。

public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Book b WHERE b.price >= :price")
    List<Book> findExpensiveBooks(@Param("price") int price);
}

FROM Book bBookは、テーブル名のbooksではなくEntityクラス名を指定する点に注意してください。

:priceのようなプレースホルダーには@Paramアノテーションで対応する引数を紐づけます。

なぜJPQLを使うのか

JPQLはEntity単位で書くため、テーブル構造が変わってもEntityのフィールド名さえ整合していればクエリの修正が最小限で済みます。

複数テーブルの結合や集計処理など、クエリメソッドの命名規則では表現しきれない複雑な条件を書けるのが最大のメリットです。

実装手順

JOINを含むJPQL

@Query("SELECT b FROM Book b JOIN b.author a WHERE a.name = :authorName")
List<Book> findByAuthorNameCustom(@Param("authorName") String authorName);

前回解説した@ManyToOneのリレーションを使って、BookからAuthorの情報を条件に含めた検索ができます。

集計関数を使う

@Query("SELECT AVG(b.price) FROM Book b WHERE b.author.id = :authorId")
Double findAveragePriceByAuthorId(@Param("authorId") Long authorId);

クエリメソッドの命名規則では表現できないAVGSUMCOUNTなどの集計関数も、JPQLなら自然に書けます。

ネイティブSQLを使う

DBの固有関数を使いたい場合や、パフォーマンスチューニングでSQLを直接制御したい場合は、nativeQuery = trueを指定します。

@Query(value = "SELECT * FROM books WHERE price >= :price ORDER BY price DESC LIMIT 10", nativeQuery = true)
List<Book> findTop10ExpensiveBooksNative(@Param("price") int price);

ネイティブSQLではEntityクラス名ではなく、実際のテーブル名・カラム名を使います。

移植性が下がる代わりに、DB固有の機能を余すことなく使えるのがメリットです。

つまずきやすいポイント

更新系クエリで例外が発生する

❌ Before:UPDATE文に@Modifyingを付け忘れる

@Query("UPDATE Book b SET b.price = :price WHERE b.id = :id")
void updatePrice(@Param("id") Long id, @Param("price") int price);

このまま実行すると、InvalidDataAccessApiUsageExceptionが発生します。

Spring Data JPAは@QueryのデフォルトをSELECT文(データ取得)とみなしているため、UPDATE・DELETE文を実行するには別途印を付ける必要があります。

筆者も最初にこのエラーメッセージを見たとき「クエリ自体は間違っていないのに、なぜ動かないのか」と原因が分からず時間を使ってしまいました。

✅ After:@Modifying@Transactionalを付ける

@Modifying
@Transactional
@Query("UPDATE Book b SET b.price = :price WHERE b.id = :id")
void updatePrice(@Param("id") Long id, @Param("price") int price);

@Modifyingでこのクエリが更新系であることを明示し、@Transactionalでトランザクション内での実行を保証します。

@TransactionalはService層のメソッドにまとめて付けておく方法もありますが、Repository側で完結させたい場合はこの形が分かりやすいです。

応用・一歩先の使い方

クエリメソッドと@Queryの使い分け

状況選ぶべき方法
単純な1〜2条件の検索クエリメソッド(命名規則)
3条件以上・可読性が落ちる場合@Query(JPQL)
集計関数・複雑なJOIN@Query(JPQL)
DB固有の関数・パフォーマンス最優先@Query(ネイティブSQL)

迷ったときは、まずクエリメソッドで書いてみて、メソッド名が3行以上になりそうなら@Queryに切り替える、という判断基準を持っておくとスムーズです。

まとめ

この記事のポイント

  • @QueryはJPQL(Entity名ベース)が基本、nativeQuery = trueで通常のSQLも書ける
  • JOINや集計関数など、クエリメソッドで表現しにくい条件は@Queryが向いている
  • UPDATE・DELETE文には@Modifying@Transactionalが必須
  • 条件がシンプルならクエリメソッド、複雑になったら@Queryという使い分けが基本

次に読むべき記事

  • クエリメソッドの命名規則
  • N+1問題とJOIN FETCHでの解決
  • Bean Validationでフォームの入力チェックを実装する

タグ: Spring Boot, 中級者向け, データベース

タイトルとURLをコピーしました