JPA クエリメソッド

このセクションでは、Spring Data JPA を使用してクエリを作成するさまざまな方法について説明します。

クエリ検索戦略

JPA モジュールは、クエリを文字列として手動で定義すること、またはメソッド名から派生させることをサポートしています。

述語 IsStartingWithStartingWithStartsWithIsEndingWithEndingWithEndsWithIsNotContainingNotContainingNotContainsIsContainingContainingContains を持つ派生クエリは、これらのクエリのそれぞれの引数がサニタイズされます。つまり、LIKE によってワイルドカードとして認識される文字が実際に引数に含まれる場合、これらはエスケープされるため、リテラルとしてのみ一致します。使用されるエスケープ文字は、@EnableJpaRepositories アノテーションの escapeCharacter を設定することにより構成できます。値式の使用と比較してください。

宣言されたクエリ

メソッド名から派生したクエリを取得することは非常に便利ですが、メソッド名パーサーが使用したいキーワードをサポートしていないか、メソッド名が不必要にくなる状況に直面するかもしれません。そのため、命名規則を使用して JPA 名前付きクエリを使用するか(詳細については JPA 名前付きクエリの使用を参照)、@Query を使用してクエリメソッドにアノテーションを付けることができます(詳細については @Query を使用するを参照)。

クエリ作成

一般に、JPA のクエリ作成メカニズムはクエリメソッドに従って機能します。次の例は、JPA クエリメソッドがどのように変換されるかを示しています。

例 1: メソッド名からのクエリ作成
public interface UserRepository extends Repository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

ここから JPA 条件 API を使用してクエリを作成しますが、本質的にこれはクエリ select u from User u where u.emailAddress = ?1 and u.lastname = ?2 に変換されます。Spring Data JPA は、プロパティ式に従って、プロパティチェックを実行し、ネストされたプロパティを走査します。

次の表は、JPA でサポートされているキーワードと、そのキーワードを含むメソッドが何に変換されるかを示しています。

表 1: メソッド名内でサポートされているキーワード
キーワード サンプル JPQL スニペット

Distinct

findDistinctByLastnameAndFirstname

select distinct …​ where x.lastname = ?1 and x.firstname = ?2

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is, Equals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1 (or … where x.firstname IS NULL if the argument is null)

Between

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

… where x.age < ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

After

findByStartDateAfter

… where x.startDate > ?1

Before

findByStartDateBefore

… where x.startDate < ?1

IsNull, Null

findByAge(Is)Null

… where x.age is null

IsNotNull, NotNull

findByAge(Is)NotNull

… where x.age is not null

Like

findByFirstnameLike

… where x.firstname like ?1

NotLike

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

findByFirstnameStartingWith

… where x.firstname like ?1 (parameter bound with appended %)

EndingWith

findByFirstnameEndingWith

… where x.firstname like ?1 (parameter bound with prepended %)

Containing

findByFirstnameContaining

… where x.firstname like ?1 (parameter bound wrapped in %)

OrderBy

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

findByLastnameNot

… where x.lastname <> ?1 (or … where x.lastname IS NOT NULL if the argument is null)

In

findByAgeIn(Collection<Age> ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection<Age> ages)

… where x.age not in ?1

True

findByActiveTrue()

… where x.active = true

False

findByActiveFalse()

… where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

… where UPPER(x.firstname) = UPPER(?1)

In および NotIn は、配列または可変引数だけでなく、Collection のサブクラスもパラメーターとして受け取ります。同じ論理演算子の他の構文バージョンについては、リポジトリクエリキーワードを確認してください。

DISTINCT は扱いにくい場合があり、必ずしも期待どおりの結果が得られるとは限りません。例: select distinct u from User u は、select distinct u.lastname from User u とはまったく異なる結果を生成します。最初のケースでは、User.id を含めているため、何も複製されません。テーブル全体が取得され、User オブジェクトになります。

ただし、後者のクエリでは、フォーカスが User.lastname のみに絞り込まれ、そのテーブルのすべての一意の姓が検索されます。この場合も、List<User> 結果セットではなく、List<String> 結果セットが生成されます。

countDistinctByLastname(String lastname) も予期しない結果を引き起こす可能性があります。Spring Data JPA は select count(distinct u.id) from User u where u.lastname = ?1 を派生させます。繰り返しになりますが、u.id は重複をヒットしないため、このクエリは、バインドされた姓を持つすべてのユーザーをカウントします。これは countByLastname(String lastname) と同じです!

このクエリのポイントは何ですか? 与えられた名前を持つ人の数を見つけるには? その拘束力のある姓を持つ明確な人々の数を見つけるには? 個別の名前の数を見つけるには? (最後のクエリはまったく異なるクエリです! ) distinct を使用するには、クエリを手動で記述し、@Query を使用して、検索する情報を最適にキャプチャーする必要がある場合があります。これは、結果セットをキャプチャーするために射影が必要になる場合もあるためです。

アノテーションベースの構成

アノテーションベースの構成には、別の構成ファイルを編集する必要がないという利点があり、メンテナンスの労力が軽減されます。新しいクエリの宣言ごとにドメインクラスを再コンパイルする必要があるため、そのメリットを享受できます。

例 2: アノテーションベースの名前付きクエリの構成
@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}

JPA 名前付きクエリの使用

例では、<named-query /> 要素と @NamedQuery アノテーションを使用しています。これらの構成要素のクエリは、JPA クエリ言語で定義する必要があります。もちろん、<named-native-query /> または @NamedNativeQuery も使用できます。これらの要素を使用すると、データベースプラットフォームの独立性が失われるため、ネイティブ SQL でクエリを定義できます。

XML 名前付きクエリ定義

XML 構成を使用するには、必要な <named-query /> 要素を、クラスパスの META-INF フォルダーにある orm.xml JPA 構成ファイルに追加します。名前付きクエリの自動呼び出しは、定義済みの命名規則を使用することで有効になります。詳細については、以下を参照してください。

例 3: XML 名前付きクエリの構成
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>

クエリには、実行時に解決するために使用される特別な名前があります。

インターフェースの宣言

これらの名前付きクエリを許可するには、UserRepository を次のように指定します。

例 4: UserRepository でのクエリメソッド宣言
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Spring Data は、これらのメソッドへの呼び出しを名前付きクエリに解決しようとします。構成されたドメインクラスの単純な名前で始まり、その後にドットで区切られたメソッド名が続きます。前述の例では、メソッド名からクエリを作成する代わりに、前に定義した名前付きクエリを使用します。

@Query を使用する

名前付きクエリを使用してエンティティのクエリを宣言することは有効なアプローチであり、少数のクエリに対しては正常に機能します。クエリ自体はそれらを実行する Java メソッドに関連付けられているため、ドメインクラスにアノテーションを付けるのではなく、Spring Data JPA @Query アノテーションを使用して実際に直接バインドできます。これにより、ドメインクラスが永続性固有の情報から解放され、クエリがリポジトリインターフェースに配置されます。

クエリメソッドにアノテーションが付けられたクエリは、@NamedQuery を使用して定義されたクエリまたは orm.xml で宣言された名前付きクエリよりも優先されます。

次の例は、@Query アノテーションを使用して作成されたクエリを示しています。

例 5: @Query を使用して、クエリメソッドでクエリを宣言する
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

QueryRewriter の適用

適用しようとする機能の数に関係なく、EntityManager に送信される前に、Spring Data JPA に必要なすべてのものをクエリに適用させることが不可能な場合があります。

クエリは EntityManager に送信される直前に取得し、「書き換え」ることができます。つまり、最後の瞬間にあらゆる変更を加えることができます。クエリの書き換えは、実際のクエリと、該当する場合はカウントクエリに適用されます。カウントクエリは最適化されているため、不要になるか、Hibernate SelectionQuery から派生するなど、他の方法でカウントが取得されます。

例 6: @Query を使用して QueryRewriter を宣言する
public interface MyRepository extends JpaRepository<User, Long> {

		@NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias",
				queryRewriter = MyQueryRewriter.class)
		List<User> findByNativeQuery(String param);

		@Query(value = "select original_user_alias from User original_user_alias",
                queryRewriter = MyQueryRewriter.class)
		List<User> findByNonNativeQuery(String param);
}

この例は、ネイティブ(純粋な SQL)リライターと JPQL クエリの両方を示しており、どちらも同じ QueryRewriter を利用しています。このシナリオでは、Spring Data JPA は、対応するタイプのアプリケーションコンテキストに登録されている Bean を探します。

次のようなクエリリライタを記述できます。

例 7: 例 QueryRewriter
public class MyQueryRewriter implements QueryRewriter {

     @Override
     public String rewrite(String query, Sort sort) {
         return query.replaceAll("original_user_alias", "rewritten_user_alias");
     }
}

Spring Framework の @Component ベースのアノテーションのいずれかを適用するか、@Configuration クラス内の @Bean メソッドの一部として使用するかにかかわらず、QueryRewriter がアプリケーションコンテキストに登録されていることを確認する必要があります。

もう 1 つのオプションは、リポジトリ自体にインターフェースを実装させることです。

例 8: QueryRewriter を提供するリポジトリ
public interface MyRepository extends JpaRepository<User, Long>, QueryRewriter {

		@Query(value = "select original_user_alias.* from SD_USER original_user_alias",
                nativeQuery = true,
				queryRewriter = MyRepository.class)
		List<User> findByNativeQuery(String param);

		@Query(value = "select original_user_alias from User original_user_alias",
                queryRewriter = MyRepository.class)
		List<User> findByNonNativeQuery(String param);

		@Override
		default String rewrite(String query, Sort sort) {
			return query.replaceAll("original_user_alias", "rewritten_user_alias");
		}
}

QueryRewriter で何をしているのかによっては、それぞれがアプリケーションコンテキストに登録されている複数のサイトを用意することをお勧めします。

CDI ベースの環境では、Spring Data JPA は BeanManager を検索して、QueryRewriter の実装のインスタンスを探します。

高度な LIKE 式の使用

次の例に示すように、@Query で作成された手動で定義されたクエリのクエリ実行メカニズムにより、クエリ定義内で高度な LIKE 式を定義できます。

例 9: @Query の高度な like 式
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname like %?1")
  List<User> findByFirstnameEndsWith(String firstname);
}

前の例では、LIKE 区切り文字(%)が認識され、クエリが有効な JPQL クエリに変換されます(% が削除されます)。クエリを実行すると、メソッド呼び出しに渡されたパラメーターは、以前に認識された LIKE パターンで拡張されます。

ネイティブクエリ

@NativeQuery アノテーションを使用すると、次の例に示すようにネイティブクエリを実行できます。

例 10: @Query を使用して、クエリメソッドでネイティブクエリを宣言します。
public interface UserRepository extends JpaRepository<User, Long> {

  @NativeQuery(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
  User findByEmailAddress(String emailAddress);
}
@NativeQuery アノテーションは主に @Query(nativeQuery=true) 用に合成されたアノテーションですが、JPA の @SqlResultSetMapping(…) を活用するために sqlResultSetMapping などの追加属性も提供します。
Spring Data は、ページ区切りと並べ替えの単純なクエリを書き換えることができます。より複雑なクエリでは、クラスパスに JSqlParser [GitHub] (英語) を配置するか、コードで countQuery を宣言する必要があります。詳細については、以下の例を参照してください。
例 11: @NativeQuery を使用して、クエリメソッドでページネーションのネイティブカウントクエリを宣言します。
public interface UserRepository extends JpaRepository<User, Long> {

  @NativeQuery(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1")
  Page<User> findByLastname(String lastname, Pageable pageable);
}

spring.properties ファイルまたはシステムプロパティを介して spring.data.jpa.query.native.parser=regex を設定することにより、クラスパス上で JSqlParser を使用できるにもかかわらず、ネイティブクエリの解析に JSqlParser の使用を無効にすることができます。

有効な値は次のとおりです (大文字と小文字は区別されません)。

  • auto (デフォルト、自動選択)

  • regex (組み込みの正規表現ベースのクエリエンハンサーを使用する)

  • jsqlparser (JSqlParser を使用する)

同様のアプローチは、クエリのコピーに .count サフィックスを追加することにより、名前付きネイティブクエリでも機能します。ただし、カウントクエリの結果セットマッピングを登録する必要があります。

マップされた結果を取得するだけでなく、ネイティブクエリでは、メソッドの戻り値の型として Map コンテナーを選択することで、データベースから生の Tuple を読み取ることができます。結果のマップには、実際のデータベース列名と値を表すキー / 値のペアが含まれます。

例 12: 生の列名と値のペアを返すネイティブクエリ
interface UserRepository extends JpaRepository<User, Long> {

  @NativeQuery("SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
  Map<String, Object> findRawMapByEmail(String emailAddress);      (1)

  @NativeQuery("SELECT * FROM USERS WHERE LASTNAME = ?1")
  List<Map<String, Object>> findRawMapByLastname(String lastname); (2)
}
1 単一の Map 結果が Tuple によってサポートされます。
2Tuple によって裏付けられた複数の Map 結果。
文字列ベースのタプルクエリは Hibernate でのみサポートされます。Eclipselink は条件ベースのタプルクエリのみをサポートします。

並べ替えの使用

ソートは、PageRequest を提供するか、Sort を直接使用することによって実行できます。Sort の Order インスタンス内で実際に使用されるプロパティは、ドメインモデルと一致する必要があります。つまり、クエリ内で使用されるプロパティまたはエイリアスのいずれかに解決する必要があります。JPQL は、これを状態フィールドパス式として定義しています。

参照不可能なパス式を使用すると、Exception になります。

ただし、Sort を @Query と組み合わせて使用すると、ORDER BY 句内の関数を含む、パスがチェックされていない Order インスタンスに忍び込むことができます。これは、Order が特定のクエリ文字列に追加されるため可能です。デフォルトでは、Spring Data JPA は関数呼び出しを含む Order インスタンスを拒否しますが、JpaSort.unsafe を使用して、潜在的に安全でない順序を追加できます。

次の例では、Sort と JpaSort を使用しています。JpaSort の安全でないオプションも含まれています。

例 13: Sort および JpaSort の使用
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.lastname like ?1%")
  List<User> findByAndSort(String lastname, Sort sort);

  @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
  List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort("lannister", Sort.by("firstname"));                (1)
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)"));            (2)
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); (3)
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len"));               (4)
1 ドメインモデルのプロパティを指す有効な Sort 式。
2 関数呼び出しを含む無効な Sort。例外をスローします。
3 明示的に安全でない  Order を含む有効 Sort
4 エイリアス機能を指す有効な Sort 式。

大きなクエリ結果のスクロール

大規模なデータセットを扱う場合、スクロールすると、すべての結果をメモリにロードせずに、結果を効率的に処理できます。

大規模なクエリ結果を使用するには、複数のオプションがあります。

  1. ページング。前の章で Pageable と PageRequest について学習しました。

  2. オフセットベースのスクロール。これは、合計結果数を必要としないため、ページングよりも軽量なバリアントです。

  3. キーセットベースのスクロール。この方法では、データベースインデックスを利用することで、オフセットベースの結果取得の欠点 (英語) を回避します。

特定のアレンジメントに最適な方法について詳しくは、こちらを参照してください。

スクロール API は、クエリメソッドクエリごとの例および Querydsl で使用できます。

文字列ベースのクエリメソッドによるスクロールはまだサポートされていません。保存された @Procedure クエリメソッドを使用したスクロールもサポートされていません。

名前付きパラメーターの使用

上記のすべての例で説明したように、デフォルトでは、Spring Data JPA は位置ベースのパラメーターバインディングを使用します。これにより、パラメーターの位置に関するリファクタリング時に、クエリメソッドが少しエラーを起こしやすくなります。この課題を解決するには、次の例に示すように、@Param アノテーションを使用してメソッドパラメーターに具体的な名前を付け、クエリ内の名前をバインドします。

例 14: 名前付きパラメーターの使用
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                 @Param("firstname") String firstname);
}
メソッドパラメーターは、定義されたクエリの順序に従って切り替えられます。
バージョン 4 の時点で、Spring は -parameters コンパイラーフラグに基づいて Java 8 のパラメーター名の検出を完全にサポートしています。デバッグ情報の代替としてビルドでこのフラグを使用することにより、名前付きパラメーターの @Param アノテーションを省略できます。

表現の使用

@Query で定義される手動で定義されたクエリでは、制限された式の使用がサポートされています。クエリが実行されると、これらの式は定義済みの変数セットに対して評価されます。

値式に慣れていない場合は、値式の基礎を参照して SpEL 式とプロパティプレースホルダーについて学びましょう。

Spring Data JPA は、entityName という変数をサポートしています。その使い方は select x from #{#entityName} x です。指定されたリポジトリに関連付けられたドメイン型の entityName を挿入します。entityName は次のように解決されます: * ドメイン型が @Entity アノテーションに name プロパティを設定している場合は、それが使用されます。* それ以外の場合は、ドメイン型の単純なクラス名が使用されます。

次の例は、クエリ文字列内の #{#entityName} 式の 1 つのユースケースを示しています。ここでは、クエリメソッドと手動で定義されたクエリでリポジトリインターフェースを定義します。

例 15: リポジトリクエリメソッドでの SpEL 式の使用: entityName
@Entity
public class User {

  @Id
  @GeneratedValue
  Long id;

  String lastname;
}

public interface UserRepository extends JpaRepository<User,Long> {

  @Query("select u from #{#entityName} u where u.lastname = ?1")
  List<User> findByLastname(String lastname);
}

@Query アノテーションのクエリ文字列に実際のエンティティ名が記述されないようにするには、#{#entityName} 変数を使用できます。

entityName は、@Entity アノテーションを使用してカスタマイズできます。orm.xml のカスタマイズは、SpEL 式ではサポートされていません。

もちろん、クエリ宣言で User を直接使用することもできますが、その場合もクエリを変更する必要があります。#entityName への参照は、User クラスの潜在的な将来の再マッピングを別のエンティティ名にピックアップします(たとえば、@Entity(name = "MyUser") を使用して)。

クエリ文字列の #{#entityName} 式のもう 1 つの使用例は、具体的なドメイン型用の特殊なリポジトリインターフェースで汎用リポジトリインターフェースを定義する場合です。具体的なインターフェースでカスタムクエリメソッドの定義を繰り返さないようにするには、次の例に示すように、汎用リポジトリインターフェースの @Query アノテーションのクエリ文字列でエンティティ名式を使用できます。

例 16: リポジトリクエリメソッドでの SpEL 式の使用: 継承付き entityName
@MappedSuperclass
public abstract class AbstractMappedType {
  …
  String attribute;
}

@Entity
public class ConcreteType extends AbstractMappedType { … }

@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
  extends Repository<T, Long> {

  @Query("select t from #{#entityName} t where t.attribute = ?1")
  List<T> findAllByAttribute(String attribute);
}

public interface ConcreteRepository
  extends MappedTypeRepository<ConcreteType> { … }

上記の例では、MappedTypeRepository インターフェースは、AbstractMappedType を継承するいくつかのドメイン型の共通の親インターフェースです。また、汎用の findAllByAttribute(…) メソッドも定義します。これは、特殊なリポジトリインターフェースのインスタンスで使用できます。ConcreteRepository で findAllByAttribute(…) を呼び出すと、クエリは select t from ConcreteType t where t.attribute = ?1 になります。

式を使用して引数を制御することもできます。式はメソッド引数の制御にも使用できます。これらの式ではエンティティ名は使用できませんが、引数は使用できます。次の例に示すように、名前またはインデックスでアクセスできます。

例 17: リポジトリクエリメソッドでの値式の使用: 引数へのアクセス
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);

like -conditions の場合、文字列値パラメーターの最初または最後に % を追加したいことがよくあります。これは、バインドパラメーターマーカーまたは SpEL 式に % を追加または接頭辞として付けることで実行できます。繰り返しますが、次の例はこれを示しています。

例 18: リポジトリクエリメソッドでの値式の使用: ワイルドカードショートカット
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);

安全でないソースからの値で like -conditions を使用する場合、ワイルドカードを含めることができないように値をサニタイズする必要があります。これにより、攻撃者は必要以上のデータを選択できるようになります。この目的のために、escape(String) メソッドが SpEL コンテキストで使用可能になります。最初の引数の _ および % のすべてのインスタンスの前に、2 番目の引数の 1 文字を付けます。JPQL および標準 SQL で使用可能な like 式の escape 句と組み合わせることで、バインドパラメーターを簡単にクリーニングできます。

例 19: リポジトリクエリメソッドでの値式の使用: 入力値のサニタイズ
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);

リポジトリインターフェースでこのメソッド宣言を指定すると、findContainingEscaped("Peter_") は Peter_Parker を検出しますが、Peter Parker は検出しません。使用するエスケープ文字は、@EnableJpaRepositories アノテーションの escapeCharacter を設定することで設定できます。SpEL コンテキストで使用可能なメソッド escape(String) は、SQL および JPQL 標準のワイルドカード _ および % のみをエスケープすることに注意してください。基盤となるデータベースまたは JPA 実装が追加のワイルドカードをサポートしている場合、これらはエスケープされません。

例 20: リポジトリクエリメソッドでの値式の使用: プロパティの構成
@Query("select u from User u where u.applicationName = ?${spring.application.name:unknown}")
List<User> findContainingEscaped(String namePart);

実行時に Environment からプロパティを解決する場合は、クエリメソッドでフォールバックを含む構成プロパティ名を参照することもできます。プロパティはクエリ実行時に評価されます。通常、プロパティプレースホルダーは文字列のような値に解決されます。

その他の方法

Spring Data JPA は、クエリを作成するためのさまざまな方法を提供します。ただし、場合によっては、クエリが単に複雑すぎて、提供される手法が適用できない場合もあります。そのような状況では、次のことを考慮してください。

  • まだ作成していない場合は、@Query を使用して自分でクエリを作成します。

  • それがニーズに合わない場合は、カスタム実装の実装を検討してください。これにより、実装を完全にユーザーに任せながら、リポジトリにメソッドを登録できます。これにより、次のことが可能になります。

    • EntityManager と直接話す (純粋な HQL/JPQL/EQL/ ネイティブ SQL を記述するか、条件 API を使用する)

    • Spring Framework の JdbcTemplate を活用する (ネイティブ SQL)

    • 別のサードパーティ製データベースツールキットを使用してください。

  • もう 1 つのオプションは、クエリをデータベース内に配置してから Spring Data JPA の @StoredProcedure アノテーションを使用するか、データベース関数の場合は @Query アノテーションを使用して CALL で呼び出すことです。

これらの戦術は、Spring Data JPA にリソース管理を提供しながら、クエリを最大限に制御する必要がある場合に最も効果的です。

クエリの変更

前のすべてのセクションでは、特定のエンティティまたはエンティティのコレクションにアクセスするためのクエリを宣言する方法について説明しました。Spring Data リポジトリのカスタム実装で説明されているカスタムメソッド機能を使用して、カスタムの変更動作を追加できます。このアプローチは包括的なカスタム機能に適しているため、次の例に示すように、クエリメソッドに @Modifying アノテーションを付けることで、パラメーターバインディングのみが必要なクエリを変更できます。

例 21: 操作クエリの宣言
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

これを行うと、メソッドにアノテーションが付けられたクエリが、選択クエリではなく更新クエリとしてトリガーされます。EntityManager には変更クエリの実行後に古いエンティティが含まれる可能性があるため、自動的にクリアしません(詳細については EntityManager.clear() の JavaDoc (英語) を参照)。これにより、EntityManager で保留中のすべての非フラッシュ変更が効果的にドロップされます。EntityManager を自動的にクリアする場合、@Modifying アノテーションの clearAutomatically 属性を true に設定できます。

@Modifying アノテーションは、@Query アノテーションとの組み合わせでのみ関連します。派生クエリメソッドまたはカスタムメソッドは、このアノテーションを必要としません。

派生削除クエリ

Spring Data JPA は、次の例に示すように、JPQL クエリを明示的に宣言する必要のない派生削除クエリもサポートしています。

例 22: 派生削除クエリを使用する
interface UserRepository extends Repository<User, Long> {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query("delete from User u where u.role.id = ?1")
  void deleteInBulkByRoleId(long roleId);
}

deleteByRoleId(…) メソッドは、基本的に deleteInBulkByRoleId(…) と同じ結果を生成するように見えますが、実行方法の点で 2 つのメソッド宣言には重要な違いがあります。名前が示すように、後者のメソッドは、データベースに対して単一の JPQL クエリ(アノテーションで定義されたもの)を発行します。これは、現在ロードされている User のインスタンスでさえ、呼び出されたライフサイクルコールバックを認識しないことを意味します。

ライフサイクルクエリが実際に呼び出されることを確認するために、deleteByRoleId(…) の呼び出しはクエリを実行し、返されたインスタンスを 1 つずつ削除して、永続性プロバイダーがそれらのエンティティで @PreRemove コールバックを実際に呼び出すことができるようにします。

実際、派生削除クエリは、クエリを実行し、結果に対して CrudRepository.delete(Iterable<User> users) を呼び出し、CrudRepository の他の delete(…) メソッドの実装との同期を維持するためのショートカットです。

多数のオブジェクトを削除する場合は、十分なメモリの可用性を確保するためにパフォーマンスへの影響を考慮する必要があります。削除されるすべてのオブジェクトは、削除される前にメモリにロードされ、フラッシュまたはトランザクションが完了するまでセッションに保持されます。

クエリヒントの適用

リポジトリインターフェースで宣言されたクエリに JPA クエリヒントを適用するには、@QueryHints アノテーションを使用できます。次の例に示すように、JPA @QueryHint アノテーションの配列とブールフラグを使用して、ページネーションを適用するときにトリガーされる追加カウントクエリに適用されるヒントを潜在的に無効にします。

例 23: QueryHints をリポジトリ方式で使用する
public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

上記の宣言は、実際のクエリに構成済みの @QueryHint を適用しますが、ページ総数を計算するためにトリガーされるカウントクエリには適用しません。

クエリへのコメントの追加

データベースのパフォーマンスに基づいてクエリをデバッグする必要がある場合があります。データベース管理者が示すクエリは、@Query を使用して記述したものとは非常に異なって見える場合があります。または、カスタムファインダーに関して Spring Data JPA が生成したと推定するものとはまったく異なる場合があります。また、例としてクエリを使用した場合も同様です。

このプロセスを簡単にするために、@Meta アノテーションを適用することにより、クエリまたはその他の操作に関係なく、ほとんどすべての JPA 操作にカスタムコメントを挿入できます。

例 24: @Meta アノテーションをリポジトリ操作に適用します
public interface RoleRepository extends JpaRepository<Role, Integer> {

	@Meta(comment = "find roles by name")
	List<Role> findByName(String name);

	@Override
	@Meta(comment = "find roles using QBE")
	<S extends Role> List<S> findAll(Example<S> example);

	@Meta(comment = "count roles for a given name")
	long countByName(String name);

	@Override
	@Meta(comment = "exists based on QBE")
	<S extends Role> boolean exists(Example<S> example);
}

このサンプルリポジトリには、カスタムファインダーが混在しているだけでなく、JpaRepository から継承された操作をオーバーライドしています。いずれにせよ、@Meta アノテーションを使用すると、データベースに送信される前にクエリに挿入される comment を追加できます。

この機能はクエリだけに限定されないことに注意することも重要です。これは、count および exists 操作にまでおよびます。また、表示されていませんが、特定の delete 操作にも拡張されます。

この機能を可能な限り適用しようとしましたが、基盤となる EntityManager の一部の操作はコメントをサポートしていません。例: entityManager.createQuery() はサポートコメントとして明確にドキュメント化されていますが、entityManager.find() 操作はそうではありません。

JPQL ロギングも SQL ロギングも JPA の標準ではないため、以下のセクションに示すように、各プロバイダーにはカスタム構成が必要です。

Hibernate コメントのアクティブ化

Hibernate でクエリコメントをアクティブにするには、hibernate.use_sql_comments を true に設定する必要があります。

Java ベースの構成設定を使用している場合、これは次のように実行できます。

例 25: Java ベースの JPA 構成
@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("hibernate.use_sql_comments", "true");
	return properties;
}

persistence.xml ファイルがある場合は、そこで適用できます。

例 26: persistence.xml ベースの構成
<persistence-unit name="my-persistence-unit">

   ...registered classes...

	<properties>
		<property name="hibernate.use_sql_comments" value="true" />
	</properties>
</persistence-unit>

最後に、Spring Boot を使用している場合は、application.properties ファイル内に設定できます。

例 27: Spring Boot プロパティベースの構成
spring.jpa.properties.hibernate.use_sql_comments=true

EclipseLink でクエリコメントをアクティブにするには、eclipselink.logging.level.sql を FINE に設定する必要があります。

Java ベースの構成設定を使用している場合、これは次のように実行できます。

例 28: Java ベースの JPA 構成
@Bean
public Properties jpaProperties() {

	Properties properties = new Properties();
	properties.setProperty("eclipselink.logging.level.sql", "FINE");
	return properties;
}

persistence.xml ファイルがある場合は、そこで適用できます。

例 29: persistence.xml ベースの構成
<persistence-unit name="my-persistence-unit">

   ...registered classes...

	<properties>
		<property name="eclipselink.logging.level.sql" value="FINE" />
	</properties>
</persistence-unit>

最後に、Spring Boot を使用している場合は、application.properties ファイル内に設定できます。

例 30: Spring Boot プロパティベースの構成
spring.jpa.properties.eclipselink.logging.level.sql=FINE

Fetch- および LoadGraphs の構成

JPA 2.1 仕様では、@NamedEntityGraph 定義を参照できる @EntityGraph アノテーションでもサポートする Fetch- および LoadGraphs の指定のサポートが導入されました。エンティティでそのアノテーションを使用して、結果のクエリのフェッチプランを構成できます。フェッチの型(Fetch または Load)は、@EntityGraph アノテーションの type 属性を使用して構成できます。詳細については、JPA 2.1 仕様 3.7.4 を参照してください。

次の例は、エンティティに名前付きエンティティグラフを定義する方法を示しています。

例 31: エンティティ上の名前付きエンティティグラフの定義。
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …
}

次の例は、リポジトリクエリメソッドで名前付きエンティティグラフを参照する方法を示しています。

例 32: リポジトリクエリメソッドで名前付きエンティティグラフ定義を参照します。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

@EntityGraph を使用して、アドホックエンティティグラフを定義することもできます。提供された attributePaths は、次の例に示すように、@NamedEntityGraph をドメイン型に明示的に追加する必要なく、対応する EntityGraph に変換されます。

例 33: リポジトリクエリメソッドでのアドホックエンティティグラフ定義の使用
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(attributePaths = { "members" })
  GroupInfo getByGroupName(String name);

}

スクロール

スクロールは、より大きな結果セットのチャンクを反復処理するための、よりきめ細かいアプローチです。スクロールは、安定ソート、スクロール型 (オフセットまたはキーセットベースのスクロール)、および結果の制限で構成されます。プロパティ名を使用して単純な並べ替え式を定義し、クエリの派生を通じて Top または First キーワードを使用して静的な結果制限を定義できます。式を連結して、複数の条件を 1 つの式にまとめることができます。

スクロールクエリは、アプリケーションがクエリ結果全体を処理するまで、要素のスクロール位置を取得して次の Window<T> を取得できる Window<T> を返します。次の結果バッチを取得して Java Iterator<List< …>> を処理するのと同様に、クエリ結果のスクロールにより、ScrollPosition から Window.positionAt(…​) にアクセスできます。

Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());

ScrollPosition は、クエリ結果全体を使用して要素の正確な位置を識別します。クエリ実行では位置パラメーターが排他的に扱われ、結果は指定された位置以降に開始されます。ScrollPosition#offset() と ScrollPosition#keyset() は、スクロール操作の開始を示す ScrollPosition の特別なバージョンです。

上記の例は、静的な並べ替えと制限を示しています。代わりに、Sort オブジェクトを受け入れるクエリメソッドを定義して、より複雑な並べ替え順序やリクエストごとの並べ替えを定義することもできます。同様に、Limit オブジェクトを提供すると、静的な制限を適用する代わりに、リクエストごとに動的な制限を定義できます。動的な並べ替えと制限の詳細については、クエリメソッドの詳細を参照してください。

WindowIterator は、次の Window の存在をチェックする必要をなくし、ScrollPosition を適用することにより、Window 間のスクロールを簡素化するユーティリティを提供します。

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.offset());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

オフセットを使用したスクロール

オフセットスクロールは、ページネーションと同様に、オフセットカウンターを使用して多数の結果をスキップし、データソースが特定のオフセットで始まる結果のみを返すようにします。この単純なメカニズムにより、大量の結果がクライアントアプリケーションに送信されるのを回避できます。ただし、ほとんどのデータベースでは、サーバーが結果を返す前に完全なクエリ結果を具体化する必要があります。

例 34: リポジトリクエリメソッドでの OffsetScrollPosition の使用
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial()); (1)
1 位置 0 の要素を含めるには、オフセットなしで開始します。

ScrollPosition.offset() と ScrollPosition.offset(0L) には違いがあります。前者はスクロール操作の開始を示し、特定のオフセットを指しませんが、後者は結果の最初の要素 (位置 0) を識別します。スクロールの排他性を考慮すると、ScrollPosition.offset(0) を使用すると最初の要素がスキップされ、1 のオフセットに変換されます。

Keyset-Filtering を使用したスクロール

オフセットベースの要件では、サーバーが結果を返す前に、ほとんどのデータベースで結果全体をマテリアライズする必要があります。そのため、クライアントはリクエストされた結果の一部しか表示しませんが、サーバーは完全な結果を構築する必要があり、追加の負荷が発生します。

Keyset-Filtering は、個々のクエリの計算と I/O 要件を削減することを目的として、データベースの組み込み機能を活用することにより、結果サブセットの取得にアプローチします。このアプローチでは、キーをクエリに渡すことでスクロールを再開するキーのセットを維持し、フィルター条件を効果的に修正します。

Keyset-Filtering の核となる考え方は、安定した並べ替え順序を使用して結果の取得を開始することです。次のチャンクにスクロールしたい場合は、並べ替えられた結果内の位置を再構築するために使用される ScrollPosition を取得します。ScrollPosition は、現在の Window 内の最後のエンティティのキーセットをキャプチャーします。クエリを実行するために、再構築によって条件句が書き直され、すべての並べ替えフィールドと主キーが含まれるようになります。これにより、データベースは潜在的なインデックスを利用してクエリを実行できるようになります。データベースは、指定されたキーセット位置からはるかに小さな結果を構築するだけでよく、大きな結果を完全にマテリアライズして特定のオフセットに到達するまで結果をスキップする必要はありません。

Keyset-Filtering では、キーセットプロパティ (並べ替えに使用されるもの) が null 非許容である必要があります。この制限は、比較演算子のストア固有の null 値の処理と、インデックス付きソースに対してクエリを実行する必要があるために適用されます。null 許容プロパティの Keyset-Filtering は、予期しない結果につながります。

リポジトリクエリメソッドでの KeysetScrollPosition の使用
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.keyset()); (1)
1 最初から開始し、追加のフィルタリングを適用しないでください。

Keyset-Filtering は、データベースに並べ替えフィールドに一致するインデックスが含まれている場合に最適に機能するため、静的並べ替えが適切に機能します。Keyset-Filtering を適用するスクロールクエリでは、並べ替え順序で使用されるプロパティがクエリによって返される必要があり、これらは返されるエンティティにマップされる必要があります。

インターフェースと DTO 射影を使用できますが、キーセット抽出の失敗を避けるために、並べ替えたすべてのプロパティを含めるようにしてください。

Sort 順序を指定するときは、クエリに関連する並べ替えプロパティを含めるだけで十分です。一意のクエリ結果を保証したくない場合は、その必要はありません。キーセットクエリメカニズムは、各クエリ結果が一意であることを保証するために、主キー (または複合主キーの残りの部分) を含めることによって並べ替え順序を修正します。