怎麼用 Spring Data 在 RESTful API 中實現更好的分頁

介紹

本文將重點介紹如何使用 Spring MVC 和 Spring Data 在 RESTful API 中實現分頁。java

REST 分頁的可發現性

在分頁範圍內,知足 REST 的 HATEOAS 約束,意味着使 API 的客戶端可以基於導航中的當前頁面發現下一頁和上一頁。 爲此,咱們將使用Link HTTP 響應頭,以及 「next」,「prev」,「first」 和 「last」 連接關係類型。
添加一個偵聽器,監聽器將檢查導航是否容許下一頁,上一頁,第一頁和最後一頁。它將相關的 URI 做爲 「連接」 添加到 HTTP 響應頭中api

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。讓咱們看看咱們如何繼續連接到下一頁:dom

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

測試分頁

代碼以下:ui

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

測試分頁的可發現性

測試的重點是當前頁面在導航中的位置,以及應該從每一個位置發現的不一樣 uri:url

@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);
}

使用 Spring Data 實現 REST 分頁

在 Spring Data 中,若是咱們須要從完整的結果集中返回一些結果,則可使用任何 Pageable 存儲庫方法,由於它將始終返回 Page。 將根據頁碼,頁面大小和排序方向返回結果。
Spring Data REST 自動識別 URL 參數,例如頁碼,頁面大小,排序等。
要使用任何存儲庫的分頁方法,咱們須要擴展 PagingAndSortingRepository:code

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

若是咱們調用 localhost:8080/subjects Spring 會自動添加頁碼,頁面大小,排序參數:對象

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

默認狀況下,頁面大小是20,可是咱們能夠經過調用相似於 localhost:8080/subject?page=10 這樣的東西來更改它。
若是咱們想實現分頁到咱們本身的自定義庫 API,咱們須要傳遞一個額外的可分頁參數,並確保 API 返回一個 Page:blog

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

每當咱們添加自定義API時,就會將 /search 端點添加到生成的連接中。所以,若是咱們調用 localhost:8080/subjects/search,咱們將看到一個分頁功能的端點:

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

全部實現 PagingAndSortingRepository 的 api 都將返回一個頁面。若是咱們須要返回來自頁面的結果列表,頁面的 getContent() API 提供了做爲 Spring Data REST API 的結果而獲取的記錄列表。

將 List 轉換爲 Page

假設咱們有一個可分頁的對象做爲輸入,可是咱們須要檢索的信息包含在一個 List 中,而不是一個 PagingAndSortingRepository。在這些狀況下,咱們可能須要將 List 轉換爲 Page。
例如,假設咱們有一個 SOAP 服務的結果列表:

List<Foo> list = getListOfFooFromSoapService();

咱們須要在發送給咱們的 Pageable 對象指定的特定位置訪問列表。那麼,讓咱們定義開始索引:

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

結束索引:

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

有了這兩個地方,咱們能夠建立一個 Page 來獲取它們之間的元素列表:

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

這樣咱們就能夠返回 Page 做爲一個有效的結果。
注意,若是咱們還但願支持排序,咱們須要在將 List 的子列表以前對其進行排序。

總結

本文演示瞭如何使用 Spring 在 REST API 中實現分頁,並討論瞭如何設置和測試可發現性。

歡迎關注個人公衆號:曲翎風,得到獨家整理的學習資源和平常乾貨推送。
若是您對個人專題內容感興趣,也能夠關注個人博客: sagowiec.com
相關文章
相關標籤/搜索