Spring Data JPAを使ってデータベースからデータを取得する際、最も頻繁に使うのがクエリメソッドです。メソッド名の命名規則に従うだけでSQLが自動生成されるため、実装クラスを書く必要がありません。しかし、初学者にとっては「どんなメソッド名を書けばいいのか」「複雑な検索条件はどう実装するのか」といった疑問が生まれやすい部分でもあります。

この記事では、Spring Data JPAのクエリメソッドの基本から、複数条件の組み合わせ、ソート・ページング、そして@Queryアノテーションを使ったカスタムクエリまで、段階的に解説します。読み終える頃には、自分で必要なクエリメソッドを実装し、適切な手法を選択できるようになっているはずです。

Spring Data JPAのクエリメソッドとは

JpaRepositoryが標準で提供するメソッド一覧

独自のクエリメソッドを書く前に、JpaRepository<T, ID> を継承するだけで使える組み込みメソッドを一覧で押さえておきましょう(Spring Boot 3.x / Spring Data JPA 3.x で動作確認)。

メソッド戻り値用途
save(entity)SINSERT または UPDATE(主キーの有無などで自動判定)
saveAll(entities)List<S>複数エンティティの保存。デフォルトでは1件ずつSQLが発行される点に注意
findById(id)Optional<T>主キーで1件取得
existsById(id)boolean主キーでの存在確認
findAll()List<T>全件取得
findAll(Pageable)Page<T>ページング・ソート付きの一覧取得
findAllById(ids)List<T>主キーのリストでまとめて取得(IN句)
count()long総件数の取得
deleteById(id) / delete(entity)void1件削除
deleteAllInBatch()void全件を1クエリで削除(1次キャッシュを経由しない)
getReferenceById(id)T遅延プロキシを返す。関連の紐付けなどSELECT不要な場面向け

findByIdOptional<T> を返すため、orElseThrow() と組み合わせるのが実務の定番パターンです。

User user = userRepository.findById(userId)
        .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId));

なお、旧バージョンで使われていた getById() / getOne() は非推奨となり、現在は getReferenceById() に統一されています。findById が即座に SELECT を発行するのに対し、getReferenceById は SQL を発行せず遅延ロードのプロキシを返します。存在チェックを兼ねてデータを取得したいときは findById、関連エンティティの紐付けだけしたい(本体のカラムは読まない)ときは getReferenceById と使い分けましょう。

また saveAll() で大量データを保存する場合、そのままでは1件ずつINSERTが発行されるため、バッチ化の設定は Spring Data JPAで大量INSERTを高速化する方法 を参照してください。

この標準メソッドだけでは足りない検索条件を実現するのが、ここから見ていくクエリメソッドです。

クエリメソッドの仕組みはシンプルです。JpaRepository を継承したインターフェースに findByName のようなメソッドを定義しておくと、Spring Data JPAが実行時にプロキシを生成し、メソッド名を解析してクエリを自動生成してくれます。

public interface UserRepository extends JpaRepository<User, Long> {
    // この時点で基本的なCRUD操作は使える
    User findByEmail(String email);
    List<User> findByAgeGreaterThan(int age);
}

クエリメソッドの基本命名規則

クエリメソッド命名規則 早見表

まず全体像を一覧で押さえておきましょう。Spring Data JPA が解析するキーワードと、生成される SQL/JPQL、メソッド名の例を対応表にまとめます。

接頭辞用途戻り値の例メソッド名の例
findByデータ取得Optional<T> / List<T>findByEmail(String)
existsBy存在確認booleanexistsByEmail(String)
countBy件数取得longcountByActive(boolean)
deleteBy削除 (要 @Transactional)void / longdeleteByStatus(String)
getBy / readBy / queryByfindBy と同義(推奨は findBy同上getByEmail(String)

getByreadByqueryByfindBy のエイリアスで、動作は完全に同じです。表記が混在すると検索性が落ちるので、チームでは最も広く使われている findBy に統一するのがおすすめです。

条件キーワードは以下のように組み合わせます。

キーワード生成される条件メソッド名の例
Andcond1 AND cond2findByNameAndEmail
Orcond1 OR cond2findByNameOrEmail
GreaterThan / GreaterThanEqual> / >=findByAgeGreaterThan
LessThan / LessThanEqual< / <=findByAgeLessThanEqual
BetweenBETWEEN ? AND ?findByAgeBetween
LikeLIKE ?(ワイルドカードは手書き)findByNameLike
ContainingLIKE %?%findByNameContaining
StartingWithLIKE ?%findByNameStartingWith
EndingWithLIKE %?findByNameEndingWith
InIN (?, ?, ...)findByStatusIn
NotInNOT IN (...)findByStatusNotIn
IsNull / IsNotNullIS NULL / IS NOT NULLfindByDeletedAtIsNull
True / False= true / = falsefindByActiveTrue
IgnoreCase大文字小文字を無視findByEmailIgnoreCase
OrderBy<Field>Asc/DescORDER BYfindByActiveOrderByNameAsc

この表を手元に置いておけば、メソッド名を組み立てるときに迷うことがなくなります。以降の節では、それぞれのカテゴリを実例つきで掘り下げていきます。

findBy / existsBy / countBy

それぞれ目的に応じて使い分けます。

public interface UserRepository extends JpaRepository<User, Long> {
    // データを取得する
    Optional<User> findByEmail(String email);
    List<User> findByName(String name);

    // 存在チェック(重複チェックなどに便利)
    boolean existsByEmail(String email);

    // 件数を取得
    long countByActive(boolean active);
}

findByで単一の結果を取得する場合は、Optional<T>を使うことでnull処理を安全に行えます。実務ではOptionalの使用が推奨されます。

deleteBy

条件に一致するレコードを削除します。削除操作には@Transactionalが必要なので注意してください。

public interface UserRepository extends JpaRepository<User, Long> {
    void deleteByStatus(String status);
    long deleteByActiveIsFalse(); // 削除した件数を返すことも可能
}

複数条件の組み合わせ(And/Or)

実務では、複数の検索条件を組み合わせることが頻繁にあります。AndOrを使って条件を組み合わせられますよ。

public interface UserRepository extends JpaRepository<User, Long> {
    // And - 全ての条件を満たす必要がある
    User findByNameAndEmail(String name, String email);
    List<User> findByActiveAndAgeGreaterThanEqual(boolean active, int age);

    // Or - いずれか一つでも満たせばマッチ
    List<User> findByNameOrEmail(String name, String email);
}

AndOrを混在させることもできますが、メソッド名が長く複雑になる場合は、後述する@Queryの使用を検討しましょう。

比較演算子を使った検索条件

Spring Data JPAは、様々な比較演算子をサポートしています。実務でよく使うパターンをまとめて見ていきましょう。

public interface UserRepository extends JpaRepository<User, Long> {
    // 数値の比較
    List<User> findByAgeGreaterThan(int age);
    List<User> findByAgeBetween(int startAge, int endAge);

    // 文字列の部分一致
    List<User> findByNameContaining(String name);  // %name%
    List<User> findByNameStartingWith(String prefix);  // prefix%

    // NULL判定
    List<User> findByProfileImageIsNull();
    List<User> findByDeletedAtIsNotNull();

    // 複数の値のいずれかに一致
    List<User> findByStatusIn(List<String> statuses);

    // Boolean型
    List<User> findByActiveTrue();
    List<User> findByActive(boolean active);  // 上記と同じ
}

ContainingStartingWithEndingWithは自動的にワイルドカードが付与されるため便利です。Likeを使う場合は、ワイルドカード(%_)を自分で含める必要があるので注意してください。

ソート(OrderBy)とページング(Pageable)

検索結果をソートしたり、ページングしたりすることは実務で頻繁に必要になりますよね。

メソッド名でソートを指定

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByActiveOrderByNameAsc(boolean active);
    List<User> findByActiveOrderByAgeDesc(boolean active);
}

Pageableで動的に指定(推奨)

メソッド名にソート順を含めると柔軟性が低くなります。実行時に動的にソート順やページサイズを指定したい場合は、Pageableパラメータを使いましょう。

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findByActive(boolean active, Pageable pageable);
}

// 使用例
Pageable pageable = PageRequest.of(page, size, Sort.by("name").descending());
Page<User> users = userRepository.findByActive(true, pageable);

Page<T>を返すと、総件数やページ数などのメタ情報も取得できます。

@Queryアノテーションによるカスタムクエリ

命名規則だけでは表現できない複雑な検索条件がある場合、@Queryアノテーションを使ってJPQL(Java Persistence Query Language)を直接記述できます。

public interface UserRepository extends JpaRepository<User, Long> {
    // 名前付きパラメータを使う(推奨)
    @Query("SELECT u FROM User u WHERE u.name = :name AND u.active = :active")
    List<User> findActiveUsersByName(@Param("name") String name,
                                      @Param("active") boolean active);

    // JOIN - 関連エンティティのプロパティを条件にする
    @Query("SELECT o FROM Order o JOIN o.user u WHERE u.name = :userName")
    List<Order> findOrdersByUserName(@Param("userName") String userName);

    // UPDATE - @Modifyingと@Transactionalが必須
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
    int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}

JPQLはSQLに似ていますが、テーブル名ではなくエンティティクラス名を使い、カラム名ではなくプロパティ名を使います。名前付きパラメータの方が可読性が高く、パラメータの順序を気にしなくて良いため推奨されます。

ネイティブクエリ(nativeQuery=true)の活用

JPQLでは表現できない、データベース固有の機能を使いたい場合は、ネイティブSQLを直接記述できます。

public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "SELECT * FROM users WHERE DATE(created_at) = :date",
           nativeQuery = true)
    List<User> findByCreatedDate(@Param("date") String date);
}

ネイティブクエリは強力ですが、データベースを変更すると動かなくなる可能性があります。データベース固有の関数や構文が必須の場合や、パフォーマンス最適化が必要な場合に検討しましょう。

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

迷ったときの判断フローはこんな感じです。

1. まずクエリメソッドで実装できるか検討

シンプルな検索条件(1〜3個程度)なら、クエリメソッドが分かりやすいです。

List<User> findByNameAndActive(String name, boolean active);

2. 複雑な条件なら @Query を検討

メソッド名が長くなりすぎる場合、JOIN、GROUP BY、集計関数が必要な場合は@Queryの方が適しています。

@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);

3. データベース固有の機能が必要ならネイティブクエリ

パフォーマンス最適化が必要な場合や、データベース固有の機能が必須の場合のみネイティブクエリを検討しましょう。

チーム開発では、この判断基準を統一しておくと良いですね。

よくあるハマりどころ

クエリメソッドを実装する際、よくあるハマりどころをいくつか紹介します。

PropertyReferenceException

エンティティに存在しないプロパティ名を指定すると、PropertyReferenceExceptionが発生します。エンティティのプロパティがusernameならfindByUsernameuserNameならfindByUserNameと、正確なキャメルケースで記述しましょう。

関連エンティティのプロパティ参照

関連エンティティのプロパティを検索条件にする場合、アンダースコア(findByUser_Name)で区切るか、@Queryを使う方が分かりやすいです。

@Modifying使用時の注意

@Modifyingを使う場合は必ず@Transactionalを付けましょう。付けないとTransactionRequiredExceptionが発生します。

デバッグ方法

実際にどんなSQLが生成されているか確認したい場合は、application.propertiesに以下を追加すると便利です。

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

実務で使える実装パターン

実際の開発でよく使われる具体的なクエリパターンをいくつか紹介します。

集計結果を取得する

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user.id = :userId")
    BigDecimal getTotalAmountByUser(@Param("userId") Long userId);

    @Query("SELECT o.user.name as userName, COUNT(o) as orderCount " +
           "FROM Order o GROUP BY o.user.id, o.user.name")
    List<UserOrderStats> getUserOrderStats();
}

DTOで必要な情報だけ取得

必要な情報だけを取得してパフォーマンスを向上させる方法です。

@Query("SELECT new com.example.dto.UserSummaryDto(u.id, u.name, u.email) " +
       "FROM User u WHERE u.active = true")
List<UserSummaryDto> findActiveUserSummaries();

まとめ

Spring Data JPAのクエリメソッドは、命名規則に従うだけでSQLが自動生成される便利な機能です。

シンプルな検索条件ならクエリメソッドを使い、複雑な条件やJOINが必要な場合は@Queryを使う、というのが基本的な使い分けですね。メソッド名が長くなりすぎたり、読みにくくなったりしたら、@Queryに切り替えるタイミングです。

さらに高度なクエリ手法(次に学ぶべきトピック)

クエリメソッドと @Query を押さえたら、実務では次の3つを順に学ぶと表現の幅が大きく広がります。本記事の範囲を超えるため、それぞれ専用記事へのリンクを置いておきます。

動的クエリ(任意条件の検索フォーム)

検索フォームのように『入力されたフィールドだけ条件に加える』タイプの動的クエリは、クエリメソッドや @Query だけでは書きにくい領域です。JpaSpecificationExecutorSpecification を組み合わせるとタイプセーフに組み立てられます。詳しくは Spring Data JPAのSpecificationで動的クエリを実装する方法 を参照してください。

N+1 問題と関連エンティティの取得最適化

findBy... でリストを取得した直後にループで関連エンティティを参照すると、N+1 クエリが発生しがちです。@EntityGraphJOIN FETCH を使った解消方法は Spring Data JPAのN+1問題を解決する方法 にまとめています。

Projection(必要なカラムだけ取得)

本文で触れた DTO Projection のほかに、Spring Data JPA はインターフェースベースの Projection もサポートしています。読み取り専用の API でレスポンスを最小化したい場合に有効で、SELECT new ... よりも記述量が少なく済みます。

NoSQL 側の同等機能

MongoDB の MongoRepository でも findBy* の命名規則はほぼ同じ語彙が使えます。RDBMS 以外への横展開を考えている場合は Spring BootでMongoDBを使う方法 も合わせて読むと、概念の対比が掴みやすくなります。

@Queryの実務パターン集 - LIKE検索・ページング・動的ソート

@Query の基本(名前付きパラメータ・JOIN・@Modifying)は本文で押さえたとおりですが、実務で検索されることの多いパターンをもう少し掘り下げておきます。

LIKE検索をパラメータで安全に書く

JPQL 内に %:keyword% のようにワイルドカードを直接埋め込む書き方は処理系によって解釈が揺れるため、CONCAT で連結する書き方が移植性が高く安全です。

@Query("SELECT u FROM User u WHERE u.name LIKE CONCAT('%', :keyword, '%')")
List<User> searchByName(@Param("keyword") String keyword);

ユーザー入力に %_ が含まれる可能性がある場合は、ESCAPE 句によるエスケープも検討してください。単純な部分一致であれば、クエリメソッドの Containing の方が短く書けることも思い出しておきましょう。

位置パラメータ(?1)と名前付きパラメータ(:name)

@Query では ?1 ?2 のような位置パラメータも使えますが、引数の順序を入れ替えただけでバグになるため、名前付きパラメータを推奨します。

// 位置パラメータ: 動くが順序依存で壊れやすい
@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.active = ?2")
List<User> findByNameAndActive(String name, boolean active);

// 名前付きパラメータ: 可読性が高く順序に依存しない(推奨)
@Query("SELECT u FROM User u WHERE u.name = :name AND u.active = :active")
List<User> findByNameAndActiveSafely(@Param("name") String name,
                                     @Param("active") boolean active);

@QueryとPageableの併用(countQuery)

@Query にも Pageable を渡してページングできます。JOIN を含むクエリでは総件数の取得が非効率になりがちなので、countQuery を明示指定するのが定石です。

@Query(value = "SELECT o FROM Order o JOIN o.user u WHERE u.active = true",
       countQuery = "SELECT COUNT(o) FROM Order o JOIN o.user u WHERE u.active = true")
Page<Order> findActiveUserOrders(Pageable pageable);

一覧APIで取得カラム自体を絞り込みたい場合は、Spring Data JPAのプロジェクションでDTOを直接取得する方法 で解説しているインターフェース射影・クラス射影が有効です。また、JOIN を含むクエリで関連エンティティも同時に読み込みたい場合の JOIN FETCH の使い方は Spring Data JPAのN+1問題を解決する方法 を参照してください。

Sortパラメータで動的にソート

ソート順だけ動的に変えたい(ページングは不要)場合は、Sort を単体で渡せます。

@Query("SELECT u FROM User u WHERE u.active = :active")
List<User> findActiveUsers(@Param("active") boolean active, Sort sort);

// 使用例
List<User> users = userRepository.findActiveUsers(true, Sort.by("createdAt").descending());

メソッド名に OrderBy を含める方式と違い、呼び出し側でソートキーを切り替えられるのが利点です。条件が4つ以上になってメソッド名が読みにくくなったら @Query へ、入力された条件だけで絞り込む検索フォームのような可変条件が必要になったら Specification へ、と段階的に乗り換えていきましょう。