1. 概要

このチュートリアルでは、SpringMVCとSpringDataを使用したRESTAPIでのページネーションの実装に焦点を当てます。

2. リソースとしてのページと表現としてのページ

RESTfulアーキテクチャのコンテキストでページネーションを設計するときの最初の質問は、ページを実際のリソースと見なすか、リソースの表現と見なすかです。

ページ自体をリソースとして扱うと、呼び出し間でリソースを一意に識別できなくなるなど、多くの問題が発生します。 これは、永続層では、ページが適切なエンティティではなく、必要に応じて構築されるホルダーであるという事実と相まって、選択を簡単にします。 ページは表現の一部です。

RESTのコンテキストでのページング設計の次の質問は、ページング情報を含める場所です。

  • URIパス内: / foo / page / 1
  • URIクエリ: / foo?page = 1

ページはリソースではないことに注意してください。ページ情報をURIにエンコードすることはオプションではありません。

この問題を解決する標準的な方法を使用して、ページング情報をURIクエリにエンコードします。

3. コントローラー

次に、実装について説明します。 ページ付け用のSpringMVCコントローラーは簡単です

@GetMapping(params = { "page", "size" })
public List<Foo> findPaginated(@RequestParam("page") int page, 
  @RequestParam("size") int size, UriComponentsBuilder uriBuilder,
  HttpServletResponse response) {
    Page<Foo> resultPage = service.findPaginated(page, size);
    if (page > resultPage.getTotalPages()) {
        throw new MyResourceNotFoundException();
    }
    eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(
      Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size));

    return resultPage.getContent();
}

この例では、@RequestParam。を介してControllerメソッドにsizepage、の2つのクエリパラメーターを挿入しています。

または、ページ、サイズ、並べ替えのパラメーターを自動的にマップするPageableオブジェクトを使用することもできます。さらに、 PagingAndSortingRepository entityは、すぐに使用できるメソッドを提供します。 Pageableをパラメーターとして使用することをサポートします。

また、カスタムイベントを介して分離しているDiscoverabilityを支援するために、HttpResponseとUriComponentsBuilderを挿入しています。 それがAPIの目標でない場合は、カスタムイベントを削除するだけです。

最後に、この記事の焦点はRESTとWebレイヤーのみであることに注意してください。 ページ付けのデータアクセス部分について詳しく知るには、SpringDataを使用したページ付けに関するこの記事を確認してください。

4. RESTページネーションの発見可能性

ページ付けの範囲内で、RESTHATEOAS制約を満たすことは、APIのクライアントが現在のページに基づいて次のおよび前のページを検出できるようにすることを意味します。ナビゲーション。 この目的のために、 Link HTTPヘッダーを、「next」、「prev」、「first」、および「last」のリンクリレーションタイプと組み合わせて使用します

RESTでは、検出可能性は横断的関心事であり、特定の操作だけでなく、操作の種類にも適用できます。 たとえば、リソースが作成されるたびに、そのリソースのURIがクライアントによって検出可能である必要があります。 この要件は任意のリソースの作成に関連しているため、個別に処理します。

RESTサービスの発見可能性に焦点を当てた前の記事で説明したように、イベントを使用してこれらの懸念を切り離します。 ページ付けの場合、イベント PaginatedResultsRetrievedEvent、がコントローラーレイヤーで発生します。 次に、このイベントのカスタムリスナーを使用して検出可能性を実装します。

つまり、リスナーは、ナビゲーションで次の前の最初の、および最後のページが許可されているかどうかを確認します。 含まれている場合は、関連するURIを「リンク」HTTPヘッダーとして応答に追加します。

それでは、ステップバイステップで進みましょう。 コントローラから渡されるUriComponentsBuilderには、ベースURL(ホスト、ポート、およびコンテキストパス)のみが含まれます。 したがって、残りのセクションを追加する必要があります。

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

    // ...
   
}

次に、StringJoinerを使用して各リンクを連結します。 uriBuilderを使用してURIを生成します。 次のページへのリンクをどのように進めるかを見てみましょう。

StringJoiner linkHeader = new StringJoiner(", ");
if (hasNextPage(page, totalPages)){
    String uriForNextPage = constructNextPageUri(uriBuilder, page, size);
    linkHeader.add(createLinkHeader(uriForNextPage, "next"));
}

constructNextPageUriメソッドのロジックを見てみましょう。

String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) {
    return uriBuilder.replaceQueryParam(PAGE, page + 1)
      .replaceQueryParam("size", size)
      .build()
      .encode()
      .toUriString();
}

含めたい残りのURIについても同様に進めます。

最後に、出力を応答ヘッダーとして追加します。

response.addHeader("Link", linkHeader.toString());

簡潔にするために、部分的なコードサンプルのみが含まれていることに注意してください。完全なコードはここにあります

5. 運転ページネーションのテスト

ページ付けと発見可能性の両方の主要なロジックは、小規模で焦点を絞った統合テストでカバーされています。 前の記事と同様に、REST保証ライブラリを使用してRESTサービスを利用し、結果を検証します。

これらは、ページ付け統合テストのいくつかの例です。 完全なテストスイートについては、GitHubプロジェクト(記事の最後にあるリンク)を確認してください。

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
    Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

    assertThat(response.getStatusCode(), is(200));
}
@Test
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
    String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2";
    Response response = RestAssured.get.get(url);

    assertThat(response.getStatusCode(), is(404));
}
@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   createResource();
   Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

   assertFalse(response.body().as(List.class).isEmpty());
}

6. 運転中のページネーションの発見可能性をテストする

ページ付けがクライアントによって検出可能であることをテストすることは比較的簡単ですが、カバーすべき多くの根拠があります。

テストでは、ナビゲーション内の現在のページの位置と、各位置から検出できる必要のあるさまざまなURIに焦点を当てます。

@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertEquals(getFooURL()+"?page=1&size=2", uriToNextPage);
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertNull(uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = RestAssured.get(getFooURL()+"?page=1&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertEquals(getFooURL()+"?page=0&size=2", uriToPrevPage);
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = RestAssured.get(getFooURL()+"?page=0&size=2");
   String uriToLastPage = extractURIByRel(first.getHeader("Link"), "last");

   Response response = RestAssured.get(uriToLastPage);

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertNull(uriToNextPage);
}

extractURIByRel、の完全な低レベルコードがrel関係によるURIの抽出を担当していることに注意してください。はここです。

7. すべてのリソースを取得する

ページ付けと検出可能性の同じトピックで、クライアントがシステム内のすべてのリソースを一度に取得できる場合、またはクライアントがページ付けされたを要求する必要がある場合に選択する必要があります。

クライアントが単一のリクエストですべてのリソースを取得できないと判断され、ページ付けが必要な場合は、リクエストを取得するための応答にいくつかのオプションを使用できます。 1つのオプションは、404( Not Found )を返し、Linkヘッダーを使用して最初のページを検出可能にすることです。

リンク= ; rel =” first”、 ; rel =” last”

もう1つのオプションは、リダイレクト303 (その他を参照)を最初のページに戻すことです。 より保守的なルートは、GET要求に対して単にクライアントに405( Method Not Allowed)を返すことです。

8. RangeHTTPヘッダーを使用したRESTページング

ページ付けを実装する比較的異なる方法は、 HTTP Rangeヘッダー、 Range Content-Range If-Range Accept-Ranges、および HTTPステータスコード、 206( Partial Content )、413( Request Entity Too Large )、および416( 要求された範囲が満たされていません)。

このアプローチの1つの見方は、HTTP範囲拡張はページ付けを目的としておらず、アプリケーションではなくサーバーによって管理される必要があるというものです。 HTTP Rangeヘッダー拡張に基づいてページネーションを実装することは技術的に可能ですが、この記事で説明した実装ほど一般的ではありません。

9. SpringDataRESTページネーション

Spring Dataでは、完全なデータセットからいくつかの結果を返す必要がある場合、常に Pageを返すため、任意のPageableリポジトリメソッドを使用できます。結果は次のようになります。ページ番号、ページサイズ、および並べ替え方向に基づいて返されます。

Spring Data REST は、ページ、サイズ、並べ替えなどのURLパラメーターを自動的に認識します。

任意のリポジトリのページングメソッドを使用するには、 PagingAndSortingRepository:を拡張する必要があります

public interface SubjectRepository extends PagingAndSortingRepository<Subject, Long>{}

http:// localhost:8080 / subjectを呼び出すと、 SpringはAPIを使用してページ、サイズ、並べ替えパラメーターの提案を自動的に追加します。

"_links" : {
  "self" : {
    "href" : "http://localhost:8080/subjects{?page,size,sort}",
    "templated" : true
  }
}

デフォルトでは、ページサイズは20ですが、 http:// localhost:8080 / subject?page=10のように呼び出すことで変更できます。

独自のカスタムリポジトリAPIにページングを実装する場合は、追加の Pageable パラメータを渡し、APIが Page:を返すことを確認する必要があります。

@RestResource(path = "nameContains")
public Page<Subject> findByNameContaining(@Param("name") String name, Pageable p);

カスタムAPIを追加するたびに、 /searchエンドポイントが生成されたリンクに追加されます。 したがって、 http:// localhost:8080 / subject / searchを呼び出すと、ページ付け対応のエンドポイントが表示されます。

"findByNameContaining" : {
  "href" : "http://localhost:8080/subjects/search/nameContains{?name,page,size,sort}",
  "templated" : true
}

PagingAndSortingRepository を実装するすべてのAPIは、ページを返します。 ページから結果のリストを返す必要がある場合は、 getContent()[ PageのX179X]APIは、Spring DataRESTAPIの結果としてフェッチされたレコードのリストを提供します。

このセクションのコードは、spring-data-restプロジェクトで利用できます。

10. リストページに変換します

入力としてPageableオブジェクトがあり、取得する必要のある情報がPagingAndSortingRepositoryではなくリストに含まれているとします。 このような場合、リストをページに変換する必要があります。

たとえば、SOAPサービスの結果のリストがあるとします。

List<Foo> list = getListOfFooFromSoapService();

送信されたPageableオブジェクトで指定された特定の位置にあるリストにアクセスする必要があります。 それでは、開始インデックスを定義しましょう。

int start = (int) pageable.getOffset();

そして終了インデックス:

int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size()
  : (start + pageable.getPageSize()));

これら2つを配置したら、ページを作成して、それらの間の要素のリストを取得できます。

Page<Foo> page 
  = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());

それでおしまい! これで、有効な結果としてpageを返すことができます。

また、並べ替えもサポートする場合は、をサブリストする前にリストを並べ替える必要があることに注意してください。

11. 結論

この記事では、Springを使用してREST APIにページネーションを実装する方法を説明し、Discoverabilityを設定してテストする方法について説明しました。

永続性レベルでのページネーションについて詳しく知りたい場合は、JPAまたはHibernateページネーションのチュートリアルを確認してください。

これらすべての例とコードスニペットの実装は、 GitHubプロジェクトにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。