REST Pagination im Frühling

REST-Paginierung im Frühjahr

1. Überblick

Dieses Tutorial konzentriert sich aufthe implementation of pagination in a REST API, using Spring MVC and Spring Data.

Weitere Lektüre:

Paginierung mit Federauflage und AngularJS-Tisch

Ein ausführlicher Einblick in die Implementierung einer einfachen API mit Paginierung in Spring und deren Verwendung in AngularJS und UI Grid.

Read more

JPA-Paginierung

Paginierung in JPA - Verwendung von JQL und der Criteria API zur korrekten Paginierung.

Read more

Erkennbarkeit der REST-API und HATEOAS

HATEOAS und Auffindbarkeit eines REST-Service - basierend auf Tests.

Read more

2. Seite als Ressource vs Seite als Darstellung

Die erste Frage beim Entwerfen der Paginierung im Kontext einer RESTful-Architektur ist, ob diepage an actual Resource or just a Representation of Resources berücksichtigt werden sollen.

Das Behandeln der Seite selbst als Ressource führt zu einer Reihe von Problemen, z. B. dass Ressourcen zwischen Aufrufen nicht mehr eindeutig identifiziert werden können. Dies, zusammen mit der Tatsache, dass die Seite in der Persistenzschicht keine richtige Entität ist, sondern ein Halter, der bei Bedarf erstellt wird, macht die Auswahl einfach:the page is part of the representation.

Die nächste Frage im Paginierungsdesign im Kontext von REST lautetwhere to include the paging information:

  • im URI-Pfad:/foo/page/1

  • die URI-Abfrage:/foo?page=1

Beachten Sie, dassa page is not a Resource das Codieren der Seiteninformationen im URI nicht mehr möglich ist.

Wir werden die Standardmethode zur Lösung dieses Problems mitencoding the paging information in a URI query. verwenden

3. Der Controller

Nun zur Implementierung -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();
}

In diesem Beispiel werden die beiden Abfrageparametersize undpage, über@RequestParam. in die Controller-Methode eingefügt

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically. Darüber hinaus bietet die EntitätPagingAndSortingRepository sofort einsatzbereite Methoden, die auch die Verwendung vonPageable als Parameter unterstützen.

Wir injizieren auch sowohl die HTTP-Antwort als auch dieUriComponentsBuilder, um die Erkennbarkeit zu verbessern - die wir über ein benutzerdefiniertes Ereignis entkoppeln. Wenn dies kein Ziel der API ist, können Sie das benutzerdefinierte Ereignis einfach entfernen.

Beachten Sie schließlich, dass der Schwerpunkt dieses Artikels nur auf dem REST und der Webebene liegt. Um tiefer in den Datenzugriffsteil der Paginierung einzusteigen, können Siecheck out this articleüber die Paginierung mit Spring-Daten sagen.

4. Auffindbarkeit für REST-Paginierung

Im Rahmen der Paginierung bedeutet das Erfüllen vonHATEOAS constraint of REST, dass der Client der API die Seitennext undpreviousbasierend auf der aktuellen Seite in der Navigation erkennen kann. Zu diesem Zweck wirdwe’re going to use the Link HTTP header, coupled with the “next“, “prev“, “first” and “last” link relation types.

In REST giltDiscoverability is a cross-cutting concern nicht nur für bestimmte Operationen, sondern auch für Arten von Operationen. Beispielsweise sollte bei jeder Erstellung einer Ressource der URI dieser Ressource vom Client erkannt werden können. Da diese Anforderung für die Erstellung einer beliebigen Ressource relevant ist, werden wir sie separat behandeln.

Wir werden diese Bedenken mithilfe von Ereignissen entkoppeln, wie inprevious article focusing on Discoverabilityeines REST-Service erläutert. Bei der Paginierung wird das Ereignis -PaginatedResultsRetrievedEvent - in der Controller-Schicht ausgelöst. Anschließend implementieren wir die Erkennbarkeit mit einem benutzerdefinierten Listener für dieses Ereignis.

Kurz gesagt, der Listener prüft, ob die Navigation Seiten mitnext,previous,first undlast zulässt. Wenn ja, wirdadd the relevant URIs to the response as a ‘Link' HTTP Header.

Gehen wir jetzt Schritt für Schritt. Die vom Controller übergebenenUriComponentsBuilder enthalten nur die Basis-URL (den Host, den Port und den Kontextpfad). Daher müssen wir die verbleibenden Abschnitte hinzufügen:

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

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

    // ...

}

Als Nächstes verwenden wirStringJoiner, um jeden Link zu verketten. Wir werden dieuriBuilder verwenden, um die URIs zu generieren. Mal sehen, wie wir mit dem Link zur Seitenextvorgehen:

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

Schauen wir uns die Logik derconstructNextPageUri-Methode an:

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

Bei den übrigen URIs, die wir einbeziehen möchten, gehen wir ähnlich vor.

Schließlich fügen wir die Ausgabe als Antwortheader hinzu:

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

Beachten Sie, dass ich der Kürze halber nur ein Teilcodebeispiel undthe full code here eingeschlossen habe.

5. Testen Sie die Fahrpaginierung

Sowohl die Hauptlogik der Paginierung als auch die Auffindbarkeit werden durch kleine, gezielte Integrationstests abgedeckt. Wie inprevious article verwenden wirREST-assured library, um den REST-Service zu nutzen und die Ergebnisse zu überprüfen.

Dies sind einige Beispiele für Paginierungsintegrationstests. Eine vollständige Testsuite finden Sie im GitHub-Projekt (Link am Ende des Artikels):

@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. Test Driving Pagination Entdeckbarkeit

Es ist relativ einfach zu testen, ob die Paginierung von einem Kunden entdeckt werden kann, obwohl es eine Menge Gründe gibt, die abgedeckt werden müssen.

The tests will focus on the position of the current page in navigation und die verschiedenen URIs, die von jeder Position aus erkennbar sein sollten:

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

Beachten Sie, dass der vollständige Low-Level-Code fürextractURIByRel - verantwortlich für das Extrahieren der URIs nachrel Relationis here ist.

7. Alle Ressourcen abrufen

Zum gleichen Thema wie Paginierung und Auffindbarkeitthe 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.

Wenn ausgewählt wird, dass der Client nicht alle Ressourcen mit einer einzelnen Anforderung abrufen kann und die Paginierung nicht optional, sondern erforderlich ist, stehen mehrere Optionen für die Antwort auf eine Anforderung zum Abrufen aller Ressourcen zur Verfügung. Eine Möglichkeit besteht darin, 404 (Not Found) zurückzugeben und den HeaderLink zu verwenden, um die erste Seite erkennbar zu machen:

Eine andere Möglichkeit besteht darin, die Umleitung - 303(See Other) - auf die erste Seite zurückzugeben. Eine konservativere Route wäre, einfach 405 (Method Not Allowed) für die GET-Anforderung an den Client zurückzugeben.

8. REST-Paging mitRange HTTP-Headern

Eine relativ andere Art der Implementierung der Paginierung besteht darin, mitHTTP Range headers -Range,Content-Range,If-Range,Accept-Ranges - undHTTP status codes - 206 zu arbeiten (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

Eine Ansicht zu diesem Ansatz ist, dass die HTTP-Bereichserweiterungen nicht für die Paginierung vorgesehen waren und dass sie vom Server und nicht von der Anwendung verwaltet werden sollten. Die Implementierung einer Paginierung basierend auf den HTTP-Range-Header-Erweiterungen ist technisch dennoch möglich, wenn auch nicht annähernd so häufig wie die in diesem Artikel beschriebene Implementierung.

9. Federdaten-REST-Paginierung

Wenn wir in Spring Data einige Ergebnisse aus dem gesamten Datensatz zurückgeben müssen, können wir die Repository-Methode vonPageableverwenden, da immerPage. zurückgegeben werden. Die Ergebnisse werden basierend auf der Seite zurückgegeben Anzahl, Seitengröße und Sortierrichtung.

Spring Data REST erkennt automatisch URL-Parameter wiepage, size, sort usw.

Um Paging-Methoden eines Repositorys zu verwenden, müssen wirPagingAndSortingRepository: erweitern

public interface SubjectRepository extends PagingAndSortingRepository{}

Wenn wirhttp://localhost:8080/subjects aufrufen, fügt sSpring automatisch die Parametervorschläge fürpage, size, sortmit der API hinzu:

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

Standardmäßig beträgt die Seitengröße 20, aber wir können sie ändern, indem wir so etwas wiehttp://localhost:8080/subjects?page=10. aufrufen

Wenn wir Paging in unsere eigene benutzerdefinierte Repository-API implementieren möchten, müssen wir einen zusätzlichenPageable-Parameter übergeben und sicherstellen, dass die API einPage: zurückgibt

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

Immer wenn wir eine benutzerdefinierte API hinzufügen, wird den generierten Links ein/search-Endpunkt hinzugefügt. Wenn wir alsohttp://localhost:8080/subjects/search we aufrufen, wird ein paginierungsfähiger Endpunkt angezeigt:

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

Alle APIs, diePagingAndSortingRepository implementieren, geben einPage. zurück. Wenn wir die Liste der Ergebnisse ausPage, zurückgeben müssen, liefert diegetContent() API vonPage die Liste von Datensätze, die als Ergebnis der Spring Data REST-API abgerufen wurden.

Der Code in diesem Abschnitt ist im Projektspring-data-restverfügbar.

10. Konvertieren Sie einList in einPage

Nehmen wir an, wir haben einPageable-Objekt als Eingabe, aber die Informationen, die wir abrufen müssen, sind in einer Liste anstelle vonPagingAndSortingRepository enthalten. In diesen Fällen benötigen wir möglicherweiseconvert a List into a Page.

Stellen Sie sich zum Beispiel vor, wir haben eine Liste der Ergebnisse einesSOAP-Dienstes:

List list = getListOfFooFromSoapService();

Wir müssen an den spezifischen Positionen auf die Liste zugreifen, die durch das an uns gesendetePageable-Objekt angegeben sind. Definieren wir also den Startindex:

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

Und der Endindex:

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

Wenn diese beiden vorhanden sind, können wir einPage erstellen, um die Liste der Elemente zwischen ihnen zu erhalten:

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

Das ist es! Wir können jetztpage als gültiges Ergebnis zurückgeben.

Und beachten Sie, dass wirsort the list before sub-listing unterstützen müssen, wenn wir auchsorting unterstützen möchten.

11. Fazit

In diesem Artikel wurde erläutert, wie die Paginierung in einer REST-API mithilfe von Spring implementiert wird, und wie Discoverability eingerichtet und getestet wird.

Wenn Sie sich eingehend mit der Paginierung in der Persistenzstufe befassen möchten, lesen Sie die Paginierungs-Tutorials zuJPA oderHibernate.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.