REST Paginering om våren

REST Topp

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

1. Oversikt

Denne opplæringen vil fokusere på implementering av paginering i et REST API ved hjelp av Spring MVC og Spring Data.

2. Side som ressurs vs side som representasjon

Det første spørsmålet når man designer paginering i sammenheng med en RESTful arkitektur er om man skal vurdere side en faktisk ressurs eller bare en representasjon av ressurser.

Å behandle selve siden som en ressurs introduserer en rekke problemer som at de ikke lenger er i stand til å identifisere ressurser mellom samtaler. Dette, kombinert med det faktum at siden i utholdenhetslaget ikke er en riktig enhet, men en holder som er konstruert når det er nødvendig, gjør valget greit: siden er en del av representasjonen.

Det neste spørsmålet i paginasjonsdesignet i sammenheng med REST er hvor du skal inkludere personsøkinformasjonen:

  • i URI-banen: / foo / side / 1
  • URI-spørringen: / foo? side = 1

Husk det en side er ikke en ressurs, koding av sideinformasjonen i URI er ikke lenger et alternativ.

Vi skal bruke den vanlige måten å løse dette problemet på koding av personsøkinformasjonen i et URI-spørsmål.

3. Kontrolleren

Nå, for implementeringen - Spring MVC Controller for pagination er grei:

@GetMapping (params = {"page", "size"}) public List findPaginated (@RequestParam ("page") int page, @RequestParam ("size") int size, UriComponentsBuilder uriBuilder, HttpServletResponse response) {Page resultPage = service .findPaginated (side, størrelse); hvis (side> resultPage.getTotalPages ()) {kast ny MyResourceNotFoundException (); } eventPublisher.publishEvent (nye PaginatedResultsRetrievedEvent (Foo.class, uriBuilder, respons, side, resultPage.getTotalPages (), størrelse)); return resultPage.getContent (); }

I dette eksemplet injiserer vi de to spørringsparametrene, størrelse og side, i Controller-metoden via @RequestParam.

Alternativt kunne vi ha brukt en Sidelig objekt, som kartlegger side, størrelse, og sortere parametrene automatisk. i tillegg PagingAndSortingRepository Enhet gir out-of-the-box-metoder som støtter bruk av Sidelig som parameter også.

Vi injiserer også både Http Response og UriComponentsBuilder for å hjelpe med Discoverability - som vi kobler fra via en tilpasset hendelse. Hvis det ikke er et mål for API-et, kan du bare fjerne den tilpassede hendelsen.

Til slutt - merk at fokuset i denne artikkelen bare er REST og nettlaget - for å gå dypere inn i datatilgangsdelen av paginering, kan du sjekke ut denne artikkelen om Pagination with Spring Data.

4. Oppdagbarhet for REST-paginering

Innenfor rammen av paginering, tilfredsstille HATEOAS begrensning av REST betyr at klienten til API-en kan oppdage neste og tidligere sider basert på gjeldende side i navigasjonen. For dette formålet, vi skal bruke Link HTTP-overskrift, kombinert med “neste“, “prev“, “først”Og”siste”Koblingsrelasjonstyper.

I REST, Oppdagbarhet er en tverrgående bekymring, gjelder ikke bare for spesifikke operasjoner, men for typer operasjoner. For eksempel, hver gang en ressurs opprettes, skal URI for den ressursen kunne oppdages av klienten. Siden dette kravet er relevant for opprettelsen av ALLE Ressurser, vil vi håndtere det separat.

Vi kobler fra disse bekymringene ved hjelp av hendelser, som vi diskuterte i forrige artikkel med fokus på Discoverability of a REST Service. I tilfelle paginering, hendelsen - PaginatedResultsRetrievedEvent - fyres i kontrollerlaget. Deretter implementerer vi oppdagbarhet med en tilpasset lytter for dette arrangementet.

Kort fortalt vil lytteren sjekke om navigasjonen tillater a neste, tidligere, først og siste sider. Hvis det gjør det - vil det legg til de aktuelle URI-ene i svaret som en 'Link' HTTP-header.

La oss gå trinn for trinn nå. De UriComponentsBuilder sendt fra kontrolleren inneholder bare basis-URL (vert, port og kontekstbane). Derfor må vi legge til de gjenværende delene:

void addLinkHeaderOnPagedResourceRetrieval (UriComponentsBuilder uriBuilder, HttpServletResponse respons, Class clazz, int page, int totalPages, int size) {String resourceName = clazz.getSimpleName (). toString (). toLowerCase (); uriBuilder.path ("/ admin /" + ressursnavn); // ...}

Deretter bruker vi en StringJoiner for å sammenkoble hver lenke. Vi bruker uriBuilder for å generere URI-ene. La oss se hvordan vi fortsetter med lenken til neste side:

StringJoiner linkHeader = ny StringJoiner (","); hvis (hasNextPage (side, totalPages)) {String uriForNextPage = constructNextPageUri (uriBuilder, side, størrelse); linkHeader.add (createLinkHeader (uriForNextPage, "neste")); }

La oss ta en titt på logikken til constructNePageUri metode:

String constructNextPageUri (UriComponentsBuilder uriBuilder, int side, int størrelse) {return uriBuilder.replaceQueryParam (PAGE, side + 1) .replaceQueryParam ("størrelse", størrelse) .build () .encode () .toUriString (); }

Vi fortsetter på samme måte for resten av URI-ene som vi ønsker å inkludere.

Til slutt vil vi legge til utdata som et svarhode:

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

Vær oppmerksom på at jeg for kortfattethet bare inkluderte en delvis kodeeksempel og hele koden her.

5. Testkjøring Paginering

Både hovedlogikken til paginering og oppdagbarhet dekkes av små, fokuserte integrasjonstester. Som i forrige artikkel, bruker vi det REST-sikre biblioteket til å konsumere REST-tjenesten og for å verifisere resultatene.

Dette er noen få eksempler på paginering integrasjonstester; for en full testpakke, sjekk ut GitHub-prosjektet (lenke på slutten av artikkelen):

@Test offentlig ugyldig nårResourcesAreRetrievedPaged_then200IsReceived () {Response response = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertThat (respons.getStatusCode (), er (200)); } @Test offentlig ugyldig nårPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived () {String url = getFooURL () + "? Page =" + randomNumeric (5) + "& size = 2"; Svarrespons = RestAssured.get.get (url); assertThat (respons.getStatusCode (), er (404)); } @Test offentlig ugyldig givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources () {createResource (); Svarrespons = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertFalse (respons.body (). som (List.class) .isEmpty ()); }

6. Test kjøring Paginering Discoverability

Å teste at paginering kan oppdages av en klient er relativt greit, selv om det er mye grunn å dekke.

Testene vil fokusere på posisjonen til den aktuelle siden i navigasjonen og de forskjellige URI-ene som skal kunne oppdages fra hver posisjon:

@Test offentlig ugyldig nårFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext () {Response response = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Streng uriToNextPage = extractURIByRel (respons.getHeader ("Link"), "neste"); assertEquals (getFooURL () + "? page = 1 & size = 2", uriToNextPage); } @Test offentlig ugyldig nårFirstPageOfResourcesAreRetrieved_thenNoPreviousPage () {Response response = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Streng uriToPrevPage = extractURIByRel (respons.getHeader ("Link"), "prev"); assertNull (uriToPrevPage); } @Test offentlig ugyldig nårSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious () {Response response = RestAssured.get (getFooURL () + "? Page = 1 & size = 2"); Streng uriToPrevPage = extractURIByRel (respons.getHeader ("Link"), "prev"); assertEquals (getFooURL () + "? page = 0 & size = 2", uriToPrevPage); } @Test offentlig ugyldig nårLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable () {Respons first = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Streng uriToLastPage = extractURIByRel (first.getHeader ("Link"), "last"); Svarrespons = RestAssured.get (uriToLastPage); Streng uriToNextPage = extractURIByRel (respons.getHeader ("Link"), "neste"); assertNull (uriToNextPage); }

Merk at hele koden på lavt nivå for extractURIByRel - ansvarlig for å utvinne URI-ene ved å rel forholdet er her.

7. Få alle ressurser

Om det samme temaet paginering og oppdagbarhet, valget må tas hvis en klient har lov til å hente alle ressursene i systemet på en gang, eller hvis klienten må be om dem paginert.

Hvis valget er gjort at klienten ikke kan hente alle ressursene med en enkelt forespørsel, og paginering ikke er valgfri, men påkrevd, er det flere alternativer tilgjengelig for svaret på en få alle forespørsler. Ett alternativ er å returnere en 404 (Ikke funnet) og bruk Link topptekst for å gjøre den første siden synlig:

Link =; rel = ”først”,; rel = ”siste”

Et annet alternativ er å returnere omdirigering - 303 (Se Annet) - til første side. En mer konservativ rute ville være å bare returnere til kunden en 405 (Metode ikke tillatt) for GET-forespørselen.

8. REST Paging med Område HTTP-overskrifter

En relativt annen måte å implementere paginering på er å jobbe med HTTP Område toppteksterOmråde, Innholdsområde, If-Range, Godta-områder - og HTTP-statuskoder – 206 (Delvis innhold), 413 (Be om enhet for stor), 416 (Forespurt rekkevidde ikke tilfredsstillende).

En visning av denne tilnærmingen er at HTTP Range-utvidelsene ikke var ment for paginering, og at de skulle administreres av serveren, ikke av applikasjonen. Implementering av paginering basert på HTTP Range header-utvidelser er likevel teknisk mulig, selv om det ikke er så vanlig som implementeringen diskutert i denne artikkelen.

9. Vårdata REST-paginering

I Spring Data, hvis vi trenger å returnere noen få resultater fra det komplette datasettet, kan vi bruke noen Sidelig depotmetode, da den alltid vil returnere a Side. Resultatene vil bli returnert basert på sidenummer, sidestørrelse og sorteringsretning.

Spring Data REST gjenkjenner automatisk URL-parametere som side, størrelse, sorter etc.

For å bruke personsøkingsmetoder i et hvilket som helst depot må vi utvide PagingAndSortingRepository:

offentlig grensesnitt SubjectRepository utvider PagingAndSortingRepository {}

Hvis vi ringer // localhost: 8080 / fag Våren legger automatisk til side, størrelse, sorter parametere forslag med API:

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

Sidestørrelsen er som standard 20, men vi kan endre den ved å ringe noe lignende // localhost: 8080 / fag? side = 10.

Hvis vi ønsker å implementere personsøk i vårt eget tilpassede depot-API, må vi sende et tillegg Sidelig parameter og sørg for at API returnerer a Side:

@RestResource (sti = "nameContains") offentlig side findByNameContaining (@Param ("name") Strengnavn, sidelig p);

Hver gang vi legger til en tilpasset API a /Søk endepunkt blir lagt til de genererte koblingene. Så hvis vi ringer // localhost: 8080 / fag / søk vi vil se et endepunkt som er kapabel til paginering:

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

Alle API-er som implementeres PagingAndSortingRepository vil returnere en Side. Hvis vi trenger å returnere listen over resultatene fra Side, de getContent () API av Side inneholder listen over poster som er hentet som et resultat av Spring Data REST API.

Koden i denne delen er tilgjengelig i vår-data-hvile-prosjektet.

10. Konverter a Liste inn i en Side

La oss anta at vi har en Sidelig objekt som input, men informasjonen som vi trenger å hente er inneholdt i en liste i stedet for a PagingAndSortingRepository. I slike tilfeller må vi kanskje konvertere en Liste inn i en Side.

Tenk deg for eksempel at vi har en liste over resultater fra en SOAP-tjeneste:

Listeliste = getListOfFooFromSoapService ();

Vi trenger tilgang til listen i de spesifikke posisjonene som er spesifisert av Sidelig objekt sendt til oss. Så la oss definere startindeksen:

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

Og sluttindeksen:

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

Når vi har disse to på plass, kan vi lage en Side for å få tak i listen over elementer mellom dem:

Sideside = ny PageImpl (fooList.subList (start, slutt), sidesidig, fooList.size ());

Det er det! Vi kan komme tilbake nå side som et gyldig resultat.

Og merk at hvis vi også vil gi støtte til sortering, må vi sorter listen før underoppføring den.

11. Konklusjon

Denne artikkelen illustrerte hvordan du implementerer Pagination i et REST API ved hjelp av Spring, og diskuterte hvordan du konfigurerer og tester Discoverability.

Hvis du vil gå i dybden med paginering i utholdenhetsnivået, sjekk ut JPA- eller Hibernate paginasjonsveiledningene mine.

Implementeringen av alle disse eksemplene og kodebitene finnes i GitHub-prosjektet - dette er et Maven-basert prosjekt, så det skal være enkelt å importere og kjøre som det er.

HVILLE bunnen

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

$config[zx-auto] not found$config[zx-overlay] not found