REST Pagination om foråret

REST Top

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN

1. Oversigt

Denne tutorial vil fokusere på implementering af pagination i en REST API ved hjælp af Spring MVC og Spring Data.

2. Side som ressource vs side som repræsentation

Det første spørgsmål ved design af pagination i sammenhæng med en RESTful arkitektur er, om man skal overveje side en faktisk ressource eller bare en repræsentation af ressourcer.

Behandling af selve siden som en ressource introducerer en lang række problemer, som f.eks. Ikke længere at være i stand til entydigt at identificere ressourcer mellem opkald. Dette kombineret med det faktum, at siden i persistenslaget ikke er en ordentlig enhed, men en holder, der er konstrueret, når det er nødvendigt, gør valget ligetil: siden er en del af repræsentationen.

Det næste spørgsmål i paginationsdesignet i forbindelse med REST er hvor man skal inkludere personsøgningsoplysninger:

  • i URI-stien: / foo / side / 1
  • URI-forespørgslen: / foo? side = 1

Husk det en side er ikke en ressource, kodning af sideoplysningerne i URI er ikke længere en mulighed.

Vi skal bruge den standard måde at løse dette problem på kodning af personsøgningsinformation i en URI-forespørgsel.

3. Controlleren

Nu til implementering - Spring MVC Controller til paginering er ligetil:

@GetMapping (params = {"side", "størrelse"}) offentlig Liste findPaginated (@RequestParam ("side") int side, @RequestParam ("størrelse") int størrelse, UriComponentsBuilder uriBuilder, HttpServletResponse svar) {Side resultPage = service .findPaginated (side, størrelse); hvis (side> resultPage.getTotalPages ()) {smid nyt MyResourceNotFoundException (); } eventPublisher.publishEvent (nye PaginatedResultsRetrievedEvent (Foo.class, uriBuilder, svar, side, resultPage.getTotalPages (), størrelse)); return resultPage.getContent (); }

I dette eksempel indsprøjter vi de to forespørgselsparametre, størrelse og side, i Controller-metoden via @RequestParam.

Alternativt kunne vi have brugt en Sidelig objekt, som kortlægger side, størrelseog sortere parametre automatisk. Hertil kommer, at PagingAndSortingRepository enhed leverer uindfriede metoder, der understøtter brug af Sidelig som en parameter også.

Vi injicerer også både Http Response og UriComponentsBuilder for at hjælpe med Discoverability - som vi afkobler via en brugerdefineret begivenhed. Hvis det ikke er et mål med API'et, kan du bare fjerne den tilpassede begivenhed.

Endelig - bemærk, at fokus i denne artikel kun er REST og weblaget - for at gå dybere ind i dataadgangsdelen af ​​pagination kan du tjekke denne artikel om Pagination with Spring Data.

4. Opdagelighed for REST-paginering

Inden for rammerne af pagination, tilfredsstillende HATEOAS-begrænsning af REST betyder at give API-klienten mulighed for at opdage Næste og Tidligere sider baseret på den aktuelle side i navigationen. Til dette formål, vi skal bruge Link HTTP-header kombineret med “Næste“, “tidligere“, “først”Og“sidst”Forbindelsesrelationstyper.

I REST, Opdagelighed er en tværgående bekymring, gælder ikke kun for specifikke operationer, men for typer af operationer. For eksempel, hver gang en ressource oprettes, skal URI'en for den ressource kunne findes af klienten. Da dette krav er relevant for oprettelsen af ​​ALLE ressourcer, håndterer vi det separat.

Vi afkobler disse bekymringer ved hjælp af begivenheder, som vi diskuterede i den foregående artikel med fokus på Discoverability of a REST Service. I tilfælde af pagination, begivenheden - PaginatedResultsRetrievedEvent - fyres i controller-laget. Derefter implementerer vi opdagelsesevne med en tilpasset lytter til denne begivenhed.

Kort sagt vil lytteren kontrollere, om navigationen muliggør en Næste, Tidligere, først og sidst sider. Hvis det gør det - vil det tilføj de relevante URI'er til svaret som en 'Link' HTTP-header.

Lad os gå trin for trin nu. Det UriComponentsBuilder videregivet fra controlleren indeholder kun basis-URL (værten, porten og kontekststien). Derfor bliver vi nødt til at tilføje de resterende sektioner:

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

Dernæst bruger vi en StringJoiner for at sammenkæde hvert link. Vi bruger uriBuilder at generere URI'erne. Lad os se, hvordan vi fortsætter med linket til Næste side:

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

Lad os se på logikken i constructNextPageUri metode:

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

Vi fortsætter på samme måde for resten af ​​URI'erne, som vi vil inkludere.

Endelig tilføjer vi output som et svarhoved:

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

Bemærk, at jeg for kortfattethed kun inkluderede en delvis kodeeksempel og den fulde kode her.

5. Testkørsel Pagination

Både hovedlogikken med pagination og opdagelighed er dækket af små, fokuserede integrationstests. Som i den foregående artikel bruger vi det REST-sikre bibliotek til at forbruge REST-tjenesten og til at verificere resultaterne.

Dette er et par eksempler på test af paginationintegration; for en komplet testpakke, se GitHub-projektet (link i slutningen af ​​artiklen):

@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 offentligt ugyldigt givetResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources () {createResource (); Svarrespons = RestAssured.get (stier.getFooURL () + "? Side = 0 & størrelse = 2"); assertFalse (respons.body (). som (List.class) .isEmpty ()); }

6. Testkørsel Pagination Opdagbarhed

At teste, at paginering kan findes af en klient, er relativt ligetil, selvom der er meget grund til at dække.

Testene fokuserer på placeringen af ​​den aktuelle side i navigationen og de forskellige URI'er, der skal kunne findes fra hver position:

@Test offentlig ugyldig nårFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext () {Response response = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Streng uriToNextPage = extractURIByRel (respons.getHeader ("Link"), "næste"); 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 () {Response first = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); String uriToLastPage = extractURIByRel (first.getHeader ("Link"), "last"); Svarrespons = RestAssured.get (uriToLastPage); Streng uriToNextPage = extractURIByRel (respons.getHeader ("Link"), "næste"); assertNull (uriToNextPage); }

Bemærk, at den fulde lavniveau-kode for ekstraktURIByRel - ansvarlig for at udvinde URI'erne ved rel forholdet er her.

7. Få alle ressourcer

Om det samme emne pagination og opdagelighed, valget skal træffes, hvis en klient har tilladelse til at hente alle ressourcerne i systemet på én gang, eller hvis klienten skal bede om dem pagineret.

Hvis valget træffes, at klienten ikke kan hente alle ressourcer med en enkelt anmodning, og paginering ikke er valgfri, men krævet, er der flere muligheder for svaret på en get all-anmodning. En mulighed er at returnere en 404 (Ikke fundet) og brug Link header for at gøre den første side synlig:

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

En anden mulighed er at returnere omdirigering - 303 (Se Andet) - til første side. En mere konservativ rute ville være at blot vende tilbage til klienten en 405 (Metode ikke tilladt) til GET-anmodningen.

8. REST Paging med Rækkevidde HTTP-overskrifter

En relativt anden måde at implementere pagination på er at arbejde med HTTP Rækkevidde overskrifterRækkevidde, Indholdsområde, Hvis-rækkevidde, Accept-intervaller - og HTTP-statuskoder – 206 (Delvist indhold), 413 (Anmod om enhed for stor), 416 (Anmodet rækkevidde ikke tilfredsstillende).

En opfattelse af denne tilgang er, at HTTP Range-udvidelser ikke var beregnet til paginering, og at de skulle administreres af serveren, ikke af applikationen. Implementering af pagination baseret på HTTP Range header-udvidelser er ikke desto mindre teknisk muligt, selvom det ikke er nær så almindeligt som implementeringen diskuteret i denne artikel.

9. Spring Data REST Pagination

I Spring Data, hvis vi har brug for at returnere et par resultater fra det komplette datasæt, kan vi bruge dem Sidelig arkivmetode, da den altid returnerer a Side. Resultaterne returneres baseret på sidenummer, sidestørrelse og sorteringsretning.

Spring Data REST genkender automatisk URL-parametre som f.eks side, størrelse, sorter etc.

For at bruge personsøgningsmetoder i ethvert arkiv skal vi udvide PagingAndSortingRepository:

offentlig grænseflade SubjectRepository udvider PagingAndSortingRepository {}

Hvis vi ringer // localhost: 8080 / emner Foråret tilføjer automatisk side, størrelse, sorter forslag til parametre med API:

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

Sidestørrelsen er som standard 20, men vi kan ændre den ved at ringe til noget lignende // localhost: 8080 / emner? side = 10.

Hvis vi vil implementere personsøgning i vores egen brugerdefinerede repository API, skal vi sende en ekstra Sidelig og sørg for, at API returnerer en Side:

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

Hver gang vi tilføjer en brugerdefineret API a /Søg slutpunkt føjes til de genererede links. Så hvis vi ringer // localhost: 8080 / emner / søgning vi vil se et slutpunkt til sideinddeling:

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

Alle API'er, der implementeres PagingAndSortingRepository vil returnere en Side. Hvis vi har brug for at returnere listen over resultater fra Side, det getContent () API af Side indeholder listen over poster, der er hentet som et resultat af Spring Data REST API.

Koden i dette afsnit er tilgængelig i foråret-dataresten-projektet.

10. Konverter a Liste ind i en Side

Lad os antage, at vi har en Sidelig objekt som input, men de oplysninger, som vi har brug for at hente, findes i en liste i stedet for en PagingAndSortingRepository. I disse tilfælde kan det være nødvendigt konvertere en Liste ind i en Side.

Forestil dig f.eks., At vi har en liste over resultater fra en SOAP-tjeneste:

Liste liste = getListOfFooFromSoapService ();

Vi er nødt til at få adgang til listen i de specifikke positioner, der er specificeret af Sidelig objekt sendt til os. Så lad os definere startindekset:

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

Og slutindekset:

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

Når vi har disse to på plads, kan vi oprette en Side for at få listen over elementer mellem dem:

Sideside = ny PageImpl (fooList.subList (start, slut), sideudsigtig, fooList.size ());

Det er det! Vi kan vende tilbage nu side som et gyldigt resultat.

Og bemærk, at hvis vi også vil give støtte til sortering, er vi nødt til det sorter listen før underfortegnelse det.

11. Konklusion

Denne artikel illustrerede, hvordan man implementerer Pagination i en REST API ved hjælp af Spring, og diskuterede, hvordan man opsætter og tester Discoverability.

Hvis du vil gå i dybden med pagination i persistensniveauet, skal du tjekke mine JPA- eller Hibernate pagination-tutorials.

Implementeringen af ​​alle disse eksempler og kodestykker findes i GitHub-projektet - dette er et Maven-baseret projekt, så det skal være let at importere og køre som det er.

REST bunden

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN