Spring Data JPAのクエリメソッド完全ガイド - 命名規則からカスタムクエリまで


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

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

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

クエリメソッドは、Spring Data JPAが提供する強力な機能の一つです。メソッド名の命名規則に従って定義するだけで、実行時にSpring Data JPAが自動的にSQLクエリを生成してくれます。

基本的な仕組みは以下の通りです。

  • JpaRepositoryを継承したインターフェースにメソッドを定義
  • メソッド名は特定の命名規則に従う(例: findByNameexistsByEmail)
  • Spring Data JPAが実行時にプロキシを生成し、メソッド名を解析してクエリを自動生成
  • 実装クラスを書く必要がない

JpaRepositoryは基本的なCRUD操作(save()findById()findAll()delete()など)を提供していますが、カスタムな検索条件が必要な場合は独自のクエリメソッドを定義します。

public interface UserRepository extends JpaRepository<User, Long> {
    // この時点で基本的なCRUD操作は使える
    // save(), findById(), findAll(), delete() など
    
    // カスタムなクエリメソッドを追加
    User findByEmail(String email);
    List<User> findByAgeGreaterThan(int age);
}

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

クエリメソッドの命名規則は、接頭辞と検索条件を組み合わせて構成されます。主な接頭辞は以下の通りです。

findBy

エンティティまたはエンティティのリストを取得します。戻り値の型によって単一取得かリスト取得かが決まります。

public interface UserRepository extends JpaRepository<User, Long> {
    // 単一のUserを取得
    User findByEmail(String email);
    
    // Userのリストを取得
    List<User> findByName(String name);
    
    // Optional<User>を使った安全なnull処理
    Optional<User> findByUsername(String username);
}

Optional<T>を使うことで、結果が見つからない場合のnull処理を安全に行えます。実務ではOptionalの使用が推奨されます。

existsBy

条件に一致するレコードが存在するかどうかをbooleanで返します。

public interface UserRepository extends JpaRepository<User, Long> {
    // メールアドレスが既に存在するかチェック
    boolean existsByEmail(String email);
    
    // ユーザー名が既に存在するかチェック
    boolean existsByUsername(String username);
}

重複チェックやバリデーションでよく使われます。

countBy

条件に一致するレコードの件数をlongで返します。

public interface UserRepository extends JpaRepository<User, Long> {
    // アクティブなユーザーの数を取得
    long countByActive(boolean active);
    
    // 特定の年齢以上のユーザー数を取得
    long countByAgeGreaterThanEqual(int age);
}

deleteBy

条件に一致するレコードを削除します。削除した件数を返すことも可能です。

public interface UserRepository extends JpaRepository<User, Long> {
    // 特定のステータスのユーザーを削除
    void deleteByStatus(String status);
    
    // 削除した件数を返す
    long deleteByActiveIsFalse();
}

削除操作には@Transactionalが必要です。詳しくはSpring Bootのトランザクション管理の記事を参照してください。

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

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

Andを使った複数条件

public interface UserRepository extends JpaRepository<User, Long> {
    // 名前とメールアドレスの両方が一致するユーザーを取得
    User findByNameAndEmail(String name, String email);
    
    // アクティブで特定の年齢以上のユーザーを取得
    List<User> findByActiveAndAgeGreaterThanEqual(boolean active, int age);
    
    // 3つの条件を組み合わせ
    List<User> findByNameAndActiveAndAgeGreaterThan(String name, boolean active, int age);
}

Andで繋がれた条件は全て満たす必要があります(SQLのAND)。

Orを使った複数条件

public interface UserRepository extends JpaRepository<User, Long> {
    // 名前またはメールアドレスが一致するユーザーを取得
    List<User> findByNameOrEmail(String name, String email);
    
    // ステータスがAまたはBのユーザーを取得
    List<User> findByStatusOrStatus(String status1, String status2);
}

Orで繋がれた条件はいずれか一つでも満たせばマッチします(SQLのOR)。

AndとOrの混在

public interface UserRepository extends JpaRepository<User, Long> {
    // (名前がXまたはY) AND アクティブ
    List<User> findByNameOrNameAndActive(String name1, String name2, boolean active);
}

AndOrを混在させる場合、優先順位に注意が必要です。メソッド名が長く複雑になる場合は、後述する@Queryの使用を検討しましょう。

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

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

LessThan / LessThanEqual / GreaterThan / GreaterThanEqual

public interface ProductRepository extends JpaRepository<Product, Long> {
    // 価格が指定額未満の商品
    List<Product> findByPriceLessThan(BigDecimal price);
    
    // 価格が指定額以下の商品
    List<Product> findByPriceLessThanEqual(BigDecimal price);
    
    // 在庫が指定数より多い商品
    List<Product> findByStockGreaterThan(int stock);
    
    // 在庫が指定数以上の商品
    List<Product> findByStockGreaterThanEqual(int stock);
}

Between

範囲検索に便利です。

public interface UserRepository extends JpaRepository<User, Long> {
    // 年齢が20歳以上30歳以下のユーザー
    List<User> findByAgeBetween(int startAge, int endAge);
    
    // 登録日が指定期間内のユーザー
    List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}

Like / NotLike / StartingWith / EndingWith / Containing

文字列の部分一致検索に使います。

public interface UserRepository extends JpaRepository<User, Long> {
    // 名前に指定文字列を含むユーザー(%value%)
    List<User> findByNameContaining(String name);
    
    // 名前が指定文字列で始まるユーザー(value%)
    List<User> findByNameStartingWith(String prefix);
    
    // 名前が指定文字列で終わるユーザー(%value)
    List<User> findByNameEndingWith(String suffix);
    
    // メールアドレスが指定パターンに一致しないユーザー
    List<User> findByEmailNotLike(String pattern);
}

Likeを使う場合は、ワイルドカード(%_)を自分で含める必要があります。一方、ContainingStartingWithEndingWithは自動的にワイルドカードが付与されるため便利です。

IsNull / IsNotNull

public interface UserRepository extends JpaRepository<User, Long> {
    // プロフィール画像が設定されていないユーザー
    List<User> findByProfileImageIsNull();
    
    // 退会日が設定されているユーザー
    List<User> findByDeletedAtIsNotNull();
}

In / NotIn

複数の値のいずれかに一致するレコードを取得します。

public interface ProductRepository extends JpaRepository<Product, Long> {
    // ステータスがリスト内のいずれかに一致する商品
    List<Product> findByStatusIn(List<String> statuses);
    
    // カテゴリーがリスト内のいずれにも一致しない商品
    List<Product> findByCategoryNotIn(List<String> categories);
}

True / False

Boolean型のプロパティを検索する際に使います。

public interface UserRepository extends JpaRepository<User, Long> {
    // アクティブなユーザー
    List<User> findByActiveTrue();
    
    // 非アクティブなユーザー
    List<User> findByActiveFalse();
    
    // 上記は以下と同じ
    List<User> findByActive(boolean active);
}

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

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

OrderByを使ったソート

メソッド名にOrderByを含めることで、ソート順を指定できます。

public interface UserRepository extends JpaRepository<User, Long> {
    // 名前の昇順でソート
    List<User> findByActiveOrderByNameAsc(boolean active);
    
    // 年齢の降順でソート
    List<User> findByActiveOrderByAgeDesc(boolean active);
    
    // 複数項目でソート(登録日の降順、次にIDの昇順)
    List<User> findByStatusOrderByCreatedAtDescIdAsc(String status);
}

Pageableパラメータを使った動的なソート・ページング

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

public interface UserRepository extends JpaRepository<User, Long> {
    // Pageableでソート・ページングを動的に指定
    Page<User> findByActive(boolean active, Pageable pageable);
    
    // リストで取得する場合もPageableは使える
    List<User> findByStatus(String status, Pageable pageable);
}

使用例:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public Page<User> getActiveUsers(int page, int size, String sortBy) {
        // ページ番号、ページサイズ、ソート条件を指定
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending());
        return userRepository.findByActive(true, pageable);
    }
}

Page<T>を返すと、総件数やページ数などのメタ情報も取得できます。ページング実装の詳細についてはSpring BootでREST APIのページネーションを実装するの記事を参照してください。

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

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

@Queryの基本的な使い方

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.name = ?1 AND u.active = ?2")
    List<User> findActiveUsersByName(String name, boolean active);
}

JPQLはSQLに似ていますが、テーブル名ではなくエンティティクラス名を使い、カラム名ではなくプロパティ名を使います。

パラメータバインディング

位置パラメータ(?1?2)の代わりに、名前付きパラメータを使うこともできます。

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);
    
    // LIKEを使った部分一致検索
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
    List<User> findByEmailDomain(@Param("domain") String domain);
}

名前付きパラメータの方が可読性が高く、パラメータの順序を気にしなくて良いため推奨されます。

JOINを使った複数テーブル検索

関連エンティティのプロパティを条件にする場合、JOINを使います。

public interface OrderRepository extends JpaRepository<Order, Long> {
    // Orderエンティティに関連するUserエンティティのnameで検索
    @Query("SELECT o FROM Order o JOIN o.user u WHERE u.name = :userName")
    List<Order> findOrdersByUserName(@Param("userName") String userName);
    
    // 複数のJOINと複雑な条件
    @Query("SELECT o FROM Order o " +
           "JOIN o.user u " +
           "JOIN o.orderItems oi " +
           "WHERE u.active = true AND oi.quantity > :minQuantity")
    List<Order> findActiveUserOrdersWithMinQuantity(@Param("minQuantity") int minQuantity);
}

エンティティの関連設定についてはSpring BootのJPAエンティティリレーションシップマッピングの記事を参照してください。

UPDATE/DELETEクエリと@Modifying

@Queryで更新や削除を行う場合は、@Modifyingアノテーションを付ける必要があります。

public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
    int deactivateInactiveUsers(@Param("date") LocalDateTime date);
    
    @Modifying
    @Transactional
    @Query("DELETE FROM User u WHERE u.deletedAt IS NOT NULL AND u.deletedAt < :date")
    int purgeDeletedUsers(@Param("date") LocalDateTime date);
}

@Modifyingを使う場合は、@Transactionalも必須です。戻り値は更新・削除された件数を返します。

ネイティブクエリ(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);
    
    // PostgreSQL固有の全文検索機能を使う例
    @Query(value = "SELECT * FROM products WHERE to_tsvector('english', name) @@ to_tsquery(:query)",
           nativeQuery = true)
    List<Product> fullTextSearch(@Param("query") String query);
}

JPQLとネイティブクエリの違い

  • JPQL: エンティティとプロパティを使う。データベース非依存。型安全。
  • ネイティブクエリ: テーブルとカラムを使う。データベース固有の機能が使える。可搬性が低い。

結果をDTOにマッピング

ネイティブクエリの結果を直接DTOにマッピングすることもできます。

public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "SELECT u.name as name, COUNT(o.id) as orderCount " +
                   "FROM users u LEFT JOIN orders o ON u.id = o.user_id " +
                   "GROUP BY u.id, u.name",
           nativeQuery = true)
    List<UserOrderSummary> getUserOrderSummary();
}

public interface UserOrderSummary {
    String getName();
    Long getOrderCount();
}

使用する場合の判断基準

ネイティブクエリは強力ですが、以下のデメリットがあります。

  • データベースを変更すると動かなくなる可能性がある
  • エンティティの変更(カラム名の変更など)が自動反映されない
  • タイプセーフでない

ネイティブクエリを使うべき場面:

  • データベース固有の関数や構文が必須の場合
  • パフォーマンス最適化が必要で、JPQLでは実現できない場合
  • 既存の複雑なSQLをそのまま使いたい場合

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

状況に応じて最適な実装方法を選択することが重要です。

クエリメソッドを使うべき場面

  • シンプルな検索条件(1〜3個程度の条件)
  • 可読性を重視したい場合
  • 型安全性を保ちたい場合
  • メソッド名から何を検索するか明確な場合
// シンプルで分かりやすい
List<User> findByNameAndActive(String name, boolean active);

@Query(JPQL)を使うべき場面

  • 複雑な検索条件(4個以上の条件、複雑なAnd/Orの組み合わせ)
  • JOIN、GROUP BY、HAVING、サブクエリが必要な場合
  • 集計関数(COUNT、SUM、AVG等)を使う場合
  • メソッド名が長くなりすぎる場合
// メソッド名が長すぎる場合は@Queryの方が分かりやすい
@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchByKeyword(@Param("keyword") String keyword);

ネイティブクエリを使うべき場面

  • データベース固有の機能が必須の場合
  • パフォーマンスの最適化が必要で、JPQLでは実現できない場合
  • 既存の複雑なSQLをそのまま使いたい場合

実務での判断フロー

  1. まずクエリメソッドで実装できるか検討
  2. メソッド名が長すぎる、または複雑な条件が必要なら@Query(JPQL)を検討
  3. データベース固有の機能が必要、またはパフォーマンス要件が厳しい場合のみネイティブクエリを検討

チーム開発では、判断基準を統一しておくことが重要です。

よくあるエラーと対処法

クエリメソッドを実装する際、初学者がよく遭遇するエラーと対処法を紹介します。

PropertyReferenceException

org.springframework.data.mapping.PropertyReferenceException: 
No property 'userName' found for type 'User'

原因: エンティティに存在しないプロパティ名を指定している。

対処法: エンティティのプロパティ名を確認し、正確なキャメルケースで記述する。例えば、エンティティのプロパティがusernameならfindByUsernameuser_nameならfindByUserName

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

関連エンティティのプロパティを検索条件にする場合の正しい書き方:

// Orderエンティティがuserプロパティ(Userエンティティ)を持つ場合
public interface OrderRepository extends JpaRepository<Order, Long> {
    // 正しい: アンダースコアで区切る
    List<Order> findByUser_Name(String userName);
    
    // または@Queryを使う(推奨)
    @Query("SELECT o FROM Order o WHERE o.user.name = :userName")
    List<Order> findByUserName(@Param("userName") String userName);
}

戻り値の型とクエリ結果の不一致

// エラー: 単一のUserを返すが、複数件ヒットする可能性がある
User findByName(String name);

対処法: 複数件ヒットする可能性がある場合はList<User>を使う。単一件を期待する場合はOptional<User>を使う。

// 正しい
Optional<User> findByEmail(String email);
List<User> findByName(String name);

@Queryでのパラメータ名の不一致

// エラー: パラメータ名が一致していない
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("userName") String name);

対処法: @Paramの値とJPQL内の:nameを一致させる。

// 正しい
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);

@Modifying使用時に@Transactionalがない

javax.persistence.TransactionRequiredException: 
Executing an update/delete query

対処法: @Modifyingを使う場合は必ず@Transactionalを付ける。

@Modifying
@Transactional  // これが必須
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
void deactivateUser(@Param("id") Long id);

デバッグ方法

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

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

これにより、コンソールに実行されたSQLとバインドされたパラメータが出力されるため、問題の特定が容易になります。

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

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

検索条件が動的に変わる場合

オプショナルな検索条件を扱う場合、Optionalパラメータを使う方法もありますが、複雑な場合はSpecificationを使うのが一般的です。

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public List<User> searchUsers(String name, Integer minAge, Boolean active) {
        // 簡易的な方法: 条件ごとにメソッドを分ける
        if (name != null && minAge != null && active != null) {
            return userRepository.findByNameContainingAndAgeGreaterThanEqualAndActive(
                name, minAge, active);
        } else if (name != null && minAge != null) {
            return userRepository.findByNameContainingAndAgeGreaterThanEqual(name, minAge);
        }
        // ... 他のパターン
        return userRepository.findAll();
    }
}

集計結果を取得する

public interface OrderRepository extends JpaRepository<Order, Long> {
    // ユーザーごとの注文数を取得
    @Query("SELECT o.user.id as userId, o.user.name as userName, COUNT(o) as orderCount " +
           "FROM Order o GROUP BY o.user.id, o.user.name")
    List<UserOrderStats> getUserOrderStats();
    
    // 特定ユーザーの注文合計金額
    @Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user.id = :userId")
    BigDecimal getTotalAmountByUser(@Param("userId") Long userId);
    
    // ステータスごとの平均金額
    @Query("SELECT o.status, AVG(o.totalAmount) FROM Order o GROUP BY o.status")
    List<Object[]> getAverageAmountByStatus();
}

public interface UserOrderStats {
    Long getUserId();
    String getUserName();
    Long getOrderCount();
}

DTOでの結果取得(コンストラクタ式)

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

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT new com.example.dto.UserSummaryDto(u.id, u.name, u.email) " +
           "FROM User u WHERE u.active = true")
    List<UserSummaryDto> findActiveUserSummaries();
}

// DTO クラス
public class UserSummaryDto {
    private Long id;
    private String name;
    private String email;
    
    public UserSummaryDto(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // getter/setter
}

バルク更新・削除

大量のレコードを効率的に更新・削除する場合に使います。

public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.lastLoginAt = :now WHERE u.id IN :userIds")
    int updateLastLoginBatch(@Param("userIds") List<Long> userIds, 
                              @Param("now") LocalDateTime now);
    
    @Modifying
    @Transactional
    @Query("DELETE FROM User u WHERE u.active = false AND u.deletedAt < :date")
    int bulkDeleteInactiveUsers(@Param("date") LocalDateTime date);
}

サブクエリを含む複雑な検索

public interface OrderRepository extends JpaRepository<Order, Long> {
    // 平均注文金額より高い注文を取得
    @Query("SELECT o FROM Order o WHERE o.totalAmount > " +
           "(SELECT AVG(o2.totalAmount) FROM Order o2)")
    List<Order> findAboveAverageOrders();
    
    // 最も多く注文しているユーザーの注文を取得
    @Query("SELECT o FROM Order o WHERE o.user.id = " +
           "(SELECT o2.user.id FROM Order o2 GROUP BY o2.user.id " +
           "ORDER BY COUNT(o2) DESC LIMIT 1)")
    List<Order> findOrdersByTopUser();
}

まとめと次のステップ

この記事では、Spring Data JPAのクエリメソッドについて、基本的な命名規則から@Queryを使った複雑なカスタムクエリまで解説しました。

重要なポイントをおさらいします。

  • クエリメソッドは命名規則に従うだけでSQLが自動生成される便利な機能
  • findByexistsBycountBydeleteByなど、目的に応じた接頭辞を使い分ける
  • AndOr、比較演算子を組み合わせて多様な検索条件を実装できる
  • OrderByやメソッド名、Pageableパラメータでソートとページングを制御できる
  • 複雑な条件では@QueryアノテーションでJPQLを記述する
  • データベース固有の機能が必要な場合はネイティブクエリを使う
  • シンプルな条件ではクエリメソッド、複雑な条件では@Queryと使い分ける

クエリメソッドをマスターすることで、データベースからのデータ取得が効率的に行えるようになります。しかし、クエリだけでなく、エンティティの設計やトランザクション管理も適切に行うことが重要です。

次のステップとして、以下のトピックも合わせて学ぶことをお勧めします。

さらに高度なクエリが必要になったら、Criteria APIやQueryDSLといったライブラリの学習も検討してみてください。これらは型安全な動的クエリの構築を可能にします。