春のRESTページ付け

春のRESTページネーション

1. 概要

このチュートリアルでは、the implementation of pagination in a REST API, using Spring MVC and Spring Data.に焦点を当てます

参考文献:

Spring RESTおよびAngularJSテーブルのページネーション

Springのページネーションを使用して簡単なAPIを実装する方法と、AngularJSとUI Gridでそれを使用する方法について詳細に説明します。

JPAページネーション

JPAのページネーション-JQLとCriteria APIを使用してページネーションを正しく行う方法。

REST APIの検出可能性とHATEOAS

HATEOASとRESTサービスの発見可能性-テストによって駆動されます。

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

RESTfulアーキテクチャのコンテキストでページネーションを設計するときの最初の質問は、page an actual Resource or just a Representation of Resourcesを考慮するかどうかです。

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

RESTのコンテキストでのページネーション設計の次の質問は、where to include the paging informationです。

  • URIパス内:/foo/page/1

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

a page is not a Resourceを念頭に置いて、URIでページ情報をエンコードすることはオプションではなくなりました。

この問題をencoding the paging information in a URI query.で解決する標準的な方法を使用します

3. コントローラー

さて、実装のために–the Spring MVC Controller for pagination is straightforward

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

    return resultPage.getContent();
}

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

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically.さらに、PagingAndSortingRepositoryエンティティは、Pageableをパラメータとして使用することもサポートするすぐに使用できるメソッドを提供します。

また、カスタムイベントを介してデカップリングしているDiscoverabilityを支援するために、Http ResponseとUriComponentsBuilderの両方を挿入しています。 それがAPIの目標でない場合は、カスタムイベントを削除するだけです。

最後に、この記事の焦点はRESTとWebレイヤーのみであることに注意してください。ページネーションのデータアクセス部分を深く掘り下げるために、Spring Dataを使用したページネーションについてcheck out this articleすることができます。

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

ページネーションの範囲内で、HATEOAS constraint of RESTを満たすことは、APIのクライアントがナビゲーションの現在のページに基づいてnextおよびpreviousページを検出できるようにすることを意味します。 この目的のために、we’re going to use the Link HTTP header, coupled with the “next“, “prev“, “first” and “last” link relation types

RESTでは、Discoverability is a cross-cutting concernは、特定の操作だけでなく、操作のタイプにも適用できます。 たとえば、リソースが作成されるたびに、そのリソースのURIはクライアントによって検出可能になります。 この要件は任意のリソースの作成に関連しているため、個別に処理します。

RESTサービスのprevious article focusing on Discoverabilityで説明したように、イベントを使用してこれらの懸念を切り離します。 ページネーションの場合、イベント–PaginatedResultsRetrievedEvent –がコントローラーレイヤーで発生します。 次に、このイベントのカスタムリスナーを使用して検出可能性を実装します。

つまり、リスナーは、ナビゲーションでnextpreviousfirst、およびlastのページが許可されているかどうかを確認します。 含まれている場合–add the relevant URIs to the response as a ‘Link' HTTP Headerになります。

さあ、一歩一歩進んでいきましょう。 コントローラから渡される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を生成します。 nextページへのリンクをどのように進めるかを見てみましょう。

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());

簡潔にするために、部分的なコードサンプルとthe full code hereのみを含めたことに注意してください。

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

ページネーションと発見可能性の両方の主要なロジックは、小規模で集中的な統合テストでカバーされています。 previous articleと同様に、REST-assured libraryを使用して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. 運転ページネーションの発見可能性のテスト

クライアントがページネーションを発見できるかどうかをテストすることは比較的簡単ですが、カバーすべき多くの根拠があります。

The tests will focus on the position of the current page in navigationおよび各位置から検出可能である必要があるさまざまな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関係is hereによってURIを抽出する責任があることに注意してください。

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

ページネーションと発見可能性の同じトピックについて、the choice must be made if a client is allowed to retrieve all the Resources in the system at once, or if the client must ask for them paginated

クライアントが単一の要求ですべてのリソースを取得できないという選択が行われ、ページネーションがオプションではなく必須である場合、すべて取得要求への応答にいくつかのオプションを使用できます。 1つのオプションは、404(Not Found)を返し、Linkヘッダーを使用して最初のページを検出可能にすることです。

もう1つのオプションは、リダイレクト– 303(See Other)–を最初のページに戻すことです。 より保守的なルートは、単にクライアントに405(GETリクエストのMethod Not Allowed))を返すことです。

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

ページネーションを実装する比較的異なる方法は、HTTP Range headersRangeContent-RangeIf-RangeAccept-Ranges –、およびHTTP status codes –206を操作することです。 (Partial Content)、413(Request Entity Too Large)、416(Requested Range Not Satisfiable)。

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

9. Spring Data RESTページネーション

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

Spring Data RESTは、page, size, sortなどのURLパラメータを自動的に認識します。

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

public interface SubjectRepository extends PagingAndSortingRepository{}

http://localhost:8080/subjects Springを呼び出すと、APIを使用してpage, size, sortパラメータの提案が自動的に追加されます。

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

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

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

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

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

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

PagingAndSortingRepositoryを実装するすべてのAPIはPage.を返しますPage,から結果のリストを返す必要がある場合、PagegetContent() APIは次のリストを提供しますSpring Data RESTAPIの結果としてフェッチされたレコード。

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

10. ListPageに変換します

入力としてPageableオブジェクトがあり、取得する必要のある情報がPagingAndSortingRepositoryではなくリストに含まれていると仮定します。 このような場合、convert a List into a Pageにする必要があります。

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

List list = getListOfFooFromSoapService();

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

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

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

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

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

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

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

また、sortingもサポートする場合は、sort the list before sub-listingにする必要があることに注意してください。

11. 結論

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

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

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