クエリメソッドの定義

リポジトリプロキシには、メソッド名からストア固有のクエリを派生させる 2 つの方法があります。

  • メソッド名から直接クエリを導出します。

  • 手動で定義されたクエリを使用します。

利用可能なオプションは、実際のストアによって異なります。ただし、作成する実際のクエリを決定する戦略が必要です。次のセクションでは、使用可能なオプションについて説明します。

クエリ検索戦略

クエリを解決するリポジトリインフラストラクチャでは、次の戦略を使用できます。XML 構成では、query-lookup-strategy 属性を使用して名前空間で戦略を構成できます。Java 構成の場合、EnableJpaRepositories アノテーションの queryLookupStrategy 属性を使用できます。特定のデータストアでは一部の戦略がサポートされていない場合があります。

  • CREATE は、クエリメソッド名からストア固有のクエリを作成しようとします。一般的なアプローチは、メソッド名から既知のプレフィックスの特定のセットを削除し、メソッドの残りを解析することです。クエリ構築の詳細については、"クエリ作成" を参照してください。

  • USE_DECLARED_QUERY は、宣言されたクエリを見つけようとし、見つからない場合は例外をスローします。クエリは、どこかのアノテーションによって定義することも、他の方法で宣言することもできます。そのストアで利用可能なオプションを見つけるには、特定のストアのドキュメントを参照してください。リポジトリインフラストラクチャがブートストラップ時にメソッドに対して宣言されたクエリを見つけられない場合、失敗します。

  • CREATE_IF_NOT_FOUND (デフォルト)は、CREATE と USE_DECLARED_QUERY を組み合わせたものです。最初に宣言されたクエリを検索し、宣言されたクエリが見つからない場合は、カスタムメソッド名ベースのクエリを作成します。これはデフォルトのルックアップ戦略であるため、明示的に何も構成しない場合に使用されます。メソッド名によるクエリ定義をすばやく行うだけでなく、必要に応じて宣言されたクエリを導入することにより、これらのクエリをカスタム調整することもできます。

クエリ作成

Spring Data リポジトリインフラストラクチャに組み込まれているクエリビルダーメカニズムは、リポジトリのエンティティに対して制約クエリを構築できます。

次の例は、いくつかのクエリを作成する方法を示しています。

メソッド名からのクエリ作成
interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

クエリメソッド名の解析は、主語と述語に分けられます。最初の部分(find … Byexists … By)はクエリのサブジェクトを定義し、2 番目の部分は述語を形成します。導入句(主語)には、さらに式を含めることができます。find (または他の導入キーワード)と By の間のテキストは、Distinct などの結果を制限するキーワードの 1 つを使用して、作成するクエリまたは Top/First は、クエリ結果を制限しますに個別のフラグを設定しない限り、説明的であると見なされます。

付録には、並べ替えや大文字小文字の修飾子を含む、クエリメソッドの主語キーワードクエリメソッドの述語キーワードの完全なリストが含まれています。ただし、最初の By は、実際の条件述語の開始を示す区切り文字として機能します。非常に基本的なレベルでは、エンティティプロパティの条件を定義し、And および Or と連結できます。

メソッドの解析の実際の結果は、クエリを作成する永続ストアによって異なります。ただし、注意すべき一般的な事項がいくつかあります。

  • 式は通常、連結可能な演算子と組み合わせたプロパティトラバーサルです。プロパティ式を AND および OR と組み合わせることができます。プロパティ式の BetweenLessThanGreaterThanLike などの演算子もサポートされます。サポートされている演算子はデータストアによって異なる可能性があるため、リファレンスドキュメントの適切な部分を参照してください。

  • メソッドパーサーは、個々のプロパティ(たとえば findByLastnameIgnoreCase(…))または大文字と小文字の区別をサポートする型のすべてのプロパティ(通常は String インスタンス - findByLastnameAndFirstnameAllIgnoreCase(…) など)の IgnoreCase フラグの設定をサポートします。ケースの無視がサポートされているかどうかはストアによって異なるため、ストア固有のクエリメソッドについては、リファレンスドキュメントの関連セクションを参照してください。

  • プロパティを参照するクエリメソッドに OrderBy 句を追加し、並べ替え方向(Asc または Desc)を提供することにより、静的な順序を適用できます。動的な並べ替えをサポートするクエリメソッドを作成するには、"ページング、大規模な結果の反復、並べ替えと制限" を参照してください。

プロパティ式

前の例に示すように、プロパティ式は管理対象エンティティの直接プロパティのみを参照できます。クエリの作成時に、解析されたプロパティが管理対象ドメインクラスのプロパティであることをすでに確認しています。ただし、ネストされたプロパティを走査して制約を定義することもできます。次のメソッドシグネチャーを検討してください。

List<Person> findByAddressZipCode(ZipCode zipCode);

Person に ZipCode を含む Address があると仮定します。その場合、メソッドは x.address.zipCode プロパティトラバーサルを作成します。解決アルゴリズムは、パーツ全体(AddressZipCode)をプロパティとして解釈することから始まり、ドメインクラスでその名前(大文字でない)のプロパティをチェックします。アルゴリズムが成功すると、そのプロパティが使用されます。そうでない場合、アルゴリズムはキャメルケース部分のソースを右側から頭と尾に分割し、対応するプロパティ(この例では、AddressZip と Code)を見つけようとします。アルゴリズムがそのヘッドを持つプロパティを見つけると、テールを取得し、そこからツリーを構築し続け、今説明したメソッドでテールを分割します。最初の分割が一致しない場合、アルゴリズムは分割ポイントを左に移動し(AddressZipCode)、続行します。

これはほとんどの場合に機能するはずですが、アルゴリズムが間違ったプロパティを選択する可能性があります。Person クラスにも addressZip プロパティがあるとします。アルゴリズムは最初の分割ラウンドですでに一致し、間違ったプロパティを選択して失敗します(addressZip の型にはおそらく code プロパティがないため)。

このあいまいさを解決するには、メソッド名内で _ を使用して、トラバーサルポイントを手動で定義します。メソッド名は次のようになります。

List<Person> findByAddress_ZipCode(ZipCode zipCode);

アンダースコア (_) は予約文字として扱われるため、標準の Java 命名規則に従うことを強くお勧めします (つまり、プロパティ名にはアンダースコアを使用せず、代わりにキャメルケースを適用します)。

アンダースコアで始まるフィールド名:

フィールド名は、String _name のようにアンダースコアで始まる場合があります。_ を _name と同様に保持し、二重 _ を使用して user__name などのネストされたパスを分割するようにしてください。

大文字のフィールド名:

すべて大文字のフィールド名はそのまま使用できます。ネストされたパス (該当する場合) は、USER_name と同様に _ を介して分割する必要があります。

2 番目の大文字を含むフィールド名:

String qCode のように、小文字で始まり、その後に大文字が続くフィールド名は、QCode のように 2 つの大文字で始めることで解決できます。パスが曖昧になる可能性があることに注意してください。

パスの曖昧さ:

次のサンプルでは、プロパティ qCode および q の配置と、code というプロパティを含む q により、パス QCode に曖昧さが生じます。

record Container(String qCode, Code q) {}
record Code(String code) {}

プロパティの直接一致が最初に考慮されるため、潜在的なネストされたパスは考慮されず、アルゴリズムは qCode フィールドを選択します。q で code フィールドを選択するには、アンダースコア表記 Q_Code が必要です。

コレクションまたはイテラブルを返すリポジトリメソッド

複数の結果を返すクエリメソッドは、標準の Java IterableListSet を使用できます。さらに、Spring Data の StreamableIterable のカスタム拡張、Vavr (英語) によって提供されるコレクション型を返すことをサポートします。考えられるすべてのクエリメソッドの戻り値の型について説明している付録を参照してください。

Streamable をクエリメソッドの戻り値の型として使用する

Iterable または任意のコレクション型の代わりに Streamable を使用できます。これは、非並列 Stream (Iterable にはない)にアクセスするための便利なメソッドと、要素を介して  … .filter(…) および  … .map(…) を直接アクセスし、Streamable を他の要素に連結する機能を提供します。

Streamable を使用してクエリメソッドの結果を結合する
interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

カスタムのストリーミング可能なラッパー型を返す

コレクション専用のラッパー型を提供することは、複数の要素を返すクエリ結果の API を提供するために一般的に使用されるパターンです。通常、これらの型は、コレクションのような型を返すリポジトリメソッドを呼び出し、ラッパー型のインスタンスを手動で作成することによって使用されます。Spring Data では、次の条件を満たす場合、これらのラッパー型をクエリメソッドの戻り値の型として使用できるため、この追加の手順を回避できます。

  1. 型は Streamable を実装します。

  2. この型は、Streamable を引数として取る of(…) または valueOf(…) という名前のコンストラクターまたは静的ファクトリメソッドのいずれかを公開します。

次のリストに例を示します。

class Product {                                         (1)
  MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {         (2)

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {                    (3)
    return streamable.stream()
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {                 (4)
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (5)
}
1 製品の価格にアクセスするための API を公開する Product エンティティ。
2Products.of(…) (Lombok アノテーションを使用して作成されたファクトリメソッド)を使用して構築できる Streamable<Product> のラッパー型。Streamable<Product> を使用する標準のコンストラクターも同様に機能します。
3 ラッパー型は追加の API を公開し、Streamable<Product> で新しい値を計算します。
4Streamable インターフェースを実装し、実際の結果に委譲します。
5 そのラッパー型 Products は、クエリメソッドの戻り値の型として直接使用できます。Streamable<Product> を返し、リポジトリクライアントでクエリの後に手動でラップする必要はありません。

Vavr コレクションのサポート

Vavr (英語) は、Java の関数型プログラミングの概念を取り入れたライブラリです。次の表に示すように、クエリメソッドの戻り値の型として使用できるコレクション型のカスタムセットが付属しています。

Vavr コレクション型 使用される Vavr 実装型 有効な Java ソース型

io.vavr.collection.Seq

io.vavr.collection.List

java.util.Iterable

io.vavr.collection.Set

io.vavr.collection.LinkedHashSet

java.util.Iterable

io.vavr.collection.Map

io.vavr.collection.LinkedHashMap

java.util.Map

実際のクエリ結果の Java 型(3 番目の列)に応じて、最初の列の型(またはそのサブ型)をクエリメソッドの戻り値の型として使用し、実装型として使用される 2 番目の列の型を取得できます。または、Traversable (Vavr Iterable と同等)を宣言して、実際の戻り値から実装クラスを導出することもできます。つまり、java.util.List は Vavr List または Seq に変換され、java.util.Set は Vavr LinkedHashSetSet に変換されます。

クエリ結果のストリーミング

戻り値の型として Java 8 Stream<T> を使用することにより、クエリメソッドの結果を段階的に処理できます。次の例に示すように、クエリ結果を Stream でラップする代わりに、データストア固有のメソッドを使用してストリーミングを実行します。

Java 8 Stream<T> を使用したクエリの結果のストリーミング
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Stream は潜在的に基礎となるデータストア固有のリソースをラップするため、使用後に閉じる必要があります。次の例に示すように、close() メソッドを使用するか、Java 7 try-with-resources ブロックを使用して、Stream を手動で閉じることができます。
Stream<T> を操作すると、try-with-resources ブロックが生成されます
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
現在、すべての Spring Data モジュールが戻り型として Stream<T> をサポートしているわけではありません。

非同期クエリ結果

Spring の非同期メソッド実行機能を使用すると、リポジトリクエリを非同期で実行できます。これは、Spring TaskExecutor に送信されたタスクで実際のクエリが発生している間、メソッドは呼び出し直後に戻ることを意味します。非同期クエリはリアクティブクエリとは異なるため、混在させないでください。リアクティブサポートの詳細については、ストア固有のドキュメントを参照してください。次の例は、いくつかの非同期クエリを示しています。

@Async
Future<User> findByFirstname(String firstname);               (1)

@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
1 戻り値の型として java.util.concurrent.Future を使用します。
2 戻り値の型として Java 8 java.util.concurrent.CompletableFuture を使用します。

ページング、大規模な結果の反復、並べ替えと制限

クエリ内のパラメーターを処理するには、前述の例で示したようにメソッドパラメーターを定義します。それに加えて、インフラストラクチャは PageableSortLimit などの特定の型を認識し、ページネーション、並べ替え、制限をクエリに動的に適用します。次の例は、これらの機能を示しています。

クエリメソッドでの PageableSliceSortLimit の使用
Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Sort sort, Limit limit);

List<User> findByLastname(String lastname, Pageable pageable);
SortPageableLimit を使用する API は、非 null 値がメソッドに渡されることを想定しています。並べ替えやページネーションを適用したくない場合は、Sort.unsorted()Pageable.unpaged()Limit.unlimited() を使用します。

最初のメソッドでは、org.springframework.data.domain.Pageable インスタンスをクエリメソッドに渡して、静的に定義されたクエリにページングを動的に追加できます。Page は、使用可能な要素とページの総数を認識しています。これは、インフラストラクチャがカウントクエリをトリガーして全体の数を計算することによって行われます。これは(使用するストアによっては)高額になる可能性があるため、代わりに Slice を返すことができます。Slice は、次の Slice が使用可能かどうかのみを認識します。これは、より大きな結果セットをウォークスルーする場合に十分な場合があります。

並べ替えオプションも Pageable インスタンスを介して処理されます。並べ替えのみが必要な場合は、メソッドに org.springframework.data.domain.Sort パラメーターを追加します。ご覧のとおり、List を返すことも可能です。この場合、実際の Page インスタンスを構築するために必要な追加のメタデータは作成されません(つまり、必要だったはずの追加のカウントクエリは発行されません)。むしろ、指定された範囲のエンティティのみを検索するようにクエリを制限します。

クエリ全体で取得するページ数を調べるには、追加のカウントクエリをトリガーする必要があります。デフォルトでは、このクエリは実際にトリガーするクエリから派生します。

特別なパラメーターは、クエリメソッド内で 1 回のみ使用できます。
上で説明したいくつかの特別なパラメーターは相互に排他的です。次の無効なパラメーターの組み合わせのリストを考慮してください。

パラメーター サンプル 理由

Pageable and Sort

findBy…​(Pageable page, Sort sort)

Pageable already defines Sort

Pageable and Limit

findBy…​(Pageable page, Limit limit)

Pageable already defines a limit.

The Top keyword used to limit results can be used to along with Pageable whereas Top defines the total maximum of results, whereas the Pageable parameter may reduce this number.

Which Method is Appropriate?

The value provided by the Spring Data abstractions is perhaps best shown by the possible query method return types outlined in the following table below. The table shows which types you can return from a query method

Table 1. Consuming Large Query Results
Method Amount of Data Fetched Query Structure Constraints

List<T>

All results.

Single query.

Query results can exhaust all memory. Fetching all data can be time-intensive.

Streamable<T>

All results.

Single query.

Query results can exhaust all memory. Fetching all data can be time-intensive.

Stream<T>

Chunked (one-by-one or in batches) depending on Stream consumption.

Single query using typically cursors.

Streams must be closed after usage to avoid resource leaks.

Flux<T>

Flux の消費量に応じてチャンク (1 つずつまたはバッチ)。

通常はカーソルを使用する単一のクエリ。

Store モジュールはリアクティブインフラストラクチャを提供する必要があります。

Slice<T>

Pageable.getPageSize() + 1 at Pageable.getOffset()

制限を適用して Pageable.getOffset() で始まるデータをフェッチする 1 対多数のクエリ。

Slice は、次の Slice にのみナビゲートできます。

  • Slice は、取得するデータがまだあるかどうかの詳細を提供します。

  • オフセットベースのクエリは、オフセットが大きすぎると非効率になります。これは、データベースが完全な結果を具体化する必要があるためです。

  • Window は、取得するデータがまだあるかどうかの詳細を提供します。

  • オフセットベースのクエリは、オフセットが大きすぎると非効率になります。これは、データベースが完全な結果を具体化する必要があるためです。

Page<T>

Pageable.getPageSize() at Pageable.getOffset()

制限を適用する Pageable.getOffset() で始まる 1 対多数のクエリ。さらに、エレメントの総数を判別するための COUNT(…) クエリが必要になる場合があります。

多くの場合、コストのかかる COUNT(…) クエリが必要になります。

  • オフセットベースのクエリは、オフセットが大きすぎると非効率になります。これは、データベースが完全な結果を具体化する必要があるためです。

ページングとソート

プロパティ名を使用して、簡単な並べ替え式を定義できます。式を連結して、複数の条件を 1 つの式に集めることができます。

ソート式の定義
Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

ソート式を定義するためのより型安全な方法については、ソート式を定義する型から始め、メソッド参照を使用してソートするプロパティを定義します。

型安全 API を使用したソート式の定義
TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());
TypedSort.by(…) は、(通常)CGlib を使用してランタイムプロキシを利用します。これは、Graal VMNative などのツールを使用するときにネイティブイメージのコンパイルを妨げる可能性があります。

ストアの実装が Querydsl をサポートしている場合は、生成されたメタモデル型を使用して、並べ替え式を定義することもできます。

QuerydslAPI を使用したソート式の定義
QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

クエリ結果の制限

ページングに加えて、専用の Limit パラメーターを使用して結果のサイズを制限することができます。First または Top キーワードを使用して、クエリメソッドの結果を制限することもできます。キーワードは同じ意味で使用できますが、Limit パラメーターと混合することはできません。Top または First にオプションの数値を追加して、返される結果の最大サイズを指定できます。数値を省略した場合、結果のサイズは 1 とみなされます。次の例は、クエリサイズを制限する方法を示しています。

Top および First を使用したクエリの結果サイズの制限
List<User> findByLastname(Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

制限式は、個別のクエリをサポートするデータストアの Distinct キーワードもサポートします。また、結果セットを 1 つのインスタンスに制限するクエリの場合、結果を Optional キーワードでラップすることがサポートされています。

ページネーションまたはスライスが制限クエリページネーション(および使用可能なページ数の計算)に適用される場合、制限された結果内で適用されます。

Sort パラメーターを使用して動的ソートと組み合わせて結果を制限すると、"K" 最小エレメントと "K" 最大エレメントの照会メソッドを表現できます。