スクロール

スクロールは、より大きな結果セットのチャンクを反復処理するための、よりきめ細かいアプローチです。スクロールは、安定ソート、スクロール型 (オフセットまたはキーセットベースのスクロール)、および結果の制限で構成されます。プロパティ名を使用して単純な並べ替え式を定義し、クエリの派生を通じて 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 の特別なバージョンです。

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
}

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

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

例 1: リポジトリクエリメソッドでの 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 の要素を含めるには、オフセットなしで開始します。

ScollPosition.offset() と ScollPosition.offset(0L) には違いがあります。前者はスクロール操作の開始を示し、特定のオフセットを指しませんが、後者は結果の最初の要素 (位置 0) を識別します。スクロールの排他性を考慮すると、ScollPosition.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 順序を指定するときは、クエリに関連する並べ替えプロパティを含めるだけで十分です。一意のクエリ結果を保証したくない場合は、その必要はありません。キーセットクエリメカニズムは、各クエリ結果が一意であることを保証するために、主キー (または複合主キーの残りの部分) を含めることによって並べ替え順序を修正します。