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操作は使える
    User findByEmail(String email);
    List<User> findByAgeGreaterThan(int age);
}

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

クエリメソッドの命名規則は、接頭辞と検索条件を組み合わせて構成されます。よく使う接頭辞を見ていきましょう。

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に切り替えるタイミングです。

クエリメソッドをマスターすることで、データベースからのデータ取得が効率的に行えるようになります。エンティティの設計やトランザクション管理についても合わせて学んでいくと、より実践的なアプリケーション開発ができるようになるでしょう。