REST-paginering in het voorjaar

REST Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

Deze tutorial zal zich concentreren op de implementatie van paginering in een REST API, met behulp van Spring MVC en Spring Data.

2. Pagina als bron versus pagina als weergave

De eerste vraag bij het ontwerpen van paginering in de context van een RESTful-architectuur is of het pagina een daadwerkelijke bron of alleen een weergave van bronnen.

Het behandelen van de pagina zelf als een hulpmiddel introduceert een groot aantal problemen, zoals het niet langer kunnen identificeren van bronnen op een unieke manier tussen aanroepen. Dit, in combinatie met het feit dat in de persistentielaag de pagina geen echte entiteit is maar een houder die wordt geconstrueerd wanneer dat nodig is, maakt de keuze eenvoudig: de pagina maakt deel uit van de voorstelling.

De volgende vraag in het pagineringontwerp in de context van REST is waar de oproepinformatie moet worden opgenomen:

  • in het URI-pad: / foo / pagina / 1
  • de URI-query: / foo? page = 1

Rekening houdende met een pagina is geen bron, is het coderen van de pagina-informatie in de URI niet langer een optie.

We gaan de standaardmanier gebruiken om dit probleem op te lossen door het coderen van de paging-informatie in een URI-query.

3. De verantwoordelijke

Nu, voor de implementatie - de Spring MVC-controller voor paginering is eenvoudig:

@GetMapping (params = {"page", "size"}) openbare lijst findPaginated (@RequestParam ("page") int-pagina, @RequestParam ("size") int size, UriComponentsBuilder uriBuilder, HttpServletResponse response) {Page resultPage = service .findPaginated (pagina, grootte); if (pagina> resultPage.getTotalPages ()) {gooi nieuwe MyResourceNotFoundException (); } eventPublisher.publishEvent (nieuwe PaginatedResultsRetrievedEvent (Foo.class, uriBuilder, reactie, pagina, resultPage.getTotalPages (), grootte)); return resultPage.getContent (); }

In dit voorbeeld injecteren we de twee queryparameters, grootte en bladzijde, in de Controller-methode via @RequestParam.

Als alternatief hadden we een Pageable object, dat het bladzijde, grootte, en soort parameters automatisch. tevens de PagingAndSortingRepository entiteit biedt kant-en-klare methoden die het gebruik van de Pageable ook als parameter.

We injecteren ook zowel de HTTP-respons als de UriComponentsBuilder om te helpen met Ontdekbaarheid - die we ontkoppelen via een evenement op maat. Als dat geen doel van de API is, kunt u de aangepaste gebeurtenis eenvoudig verwijderen.

Tot slot - merk op dat de focus van dit artikel alleen de REST en de weblaag is - om dieper in te gaan op het gegevenstoegangsgedeelte van paginering, kun je dit artikel lezen over Paginering met Spring Data.

4. Vindbaarheid voor REST-paginering

Binnen het bereik van paginering, voldoet aan de HATEOAS beperking van REST betekent dat de client van de API het De volgende en vorige pagina's op basis van de huidige pagina in de navigatie. Voor dit doeleinde, we gaan de Koppeling HTTP-header, gekoppeld aan de "De volgende“, “vorige“, “eerste"En"laatste”Linkrelatiesoorten.

In rust, Vindbaarheid is een transversale zorg, niet alleen van toepassing op specifieke operaties, maar ook op soorten operaties. Elke keer dat een resource wordt gemaakt, moet de URI van die resource bijvoorbeeld door de client kunnen worden gedetecteerd. Aangezien deze vereiste relevant is voor het maken van ELKE bron, behandelen we deze afzonderlijk.

We zullen deze zorgen loskoppelen met behulp van gebeurtenissen, zoals we hebben besproken in het vorige artikel over de vindbaarheid van een REST-service. In het geval van paginering, de gebeurtenis - PaginatedResultsRetrievedEvent - wordt afgevuurd in de controllerlaag. Vervolgens implementeren we vindbaarheid met een aangepaste luisteraar voor dit evenement.

Kortom, de luisteraar zal controleren of de navigatie een De volgende, vorige, eerste en laatste Pagina's. Als dat zo is, zal het gebeuren voeg de relevante URI's toe aan het antwoord als een ‘Link'-HTTP-header.

Laten we nu stap voor stap gaan. De UriComponentsBuilder doorgegeven van de controller bevat alleen de basis-URL (de host, de poort en het contextpad). Daarom moeten we de resterende secties toevoegen:

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

Vervolgens gebruiken we een StringJoiner om elke link samen te voegen. We gebruiken de uriBuilder om de URI's te genereren. Laten we eens kijken hoe we verder zouden gaan met de link naar het De volgende bladzijde:

StringJoiner linkHeader = nieuwe StringJoiner (","); if (hasNextPage (pagina, totalPages)) {String uriForNextPage = constructNextPageUri (uriBuilder, pagina, grootte); linkHeader.add (createLinkHeader (uriForNextPage, "next")); }

Laten we eens kijken naar de logica van het constructNextPageUri methode:

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

We gaan op dezelfde manier te werk voor de rest van de URI's die we willen opnemen.

Ten slotte voegen we de uitvoer toe als een antwoordkop:

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

Merk op dat ik, voor de beknoptheid, hier slechts een gedeeltelijk codevoorbeeld en de volledige code heb opgenomen.

5. Paginering van testritten

Zowel de hoofdlogica van paginering als vindbaarheid worden gedekt door kleine, gerichte integratietests. Net als in het vorige artikel, gebruiken we de REST-verzekerde bibliotheek om de REST-service te gebruiken en om de resultaten te verifiëren.

Dit zijn een paar voorbeelden van integratietests voor paginering; Bekijk voor een volledige testsuite het GitHub-project (link aan het einde van het artikel):

@Test openbare leegte whenResourcesAreRetrievedPaged_then200IsReceived () {Response response = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertThat (response.getStatusCode (), is (200)); } @Test openbare leegte whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived () {String url = getFooURL () + "? Page =" + randomNumeric (5) + "& size = 2"; Antwoordantwoord = RestAssured.get.get (url); assertThat (response.getStatusCode (), is (404)); } @Test openbare leegte gegevenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources () {createResource (); Antwoordantwoord = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertFalse (response.body (). as (List.class) .isEmpty ()); }

6. Test Driving Pagination Discoverability

Testen dat de paginering door een klant kan worden ontdekt, is relatief eenvoudig, hoewel er nog veel te doen is.

De tests zullen zich concentreren op de positie van de huidige pagina in navigatie en de verschillende URI's die vanuit elke positie detecteerbaar zouden moeten zijn:

@Test openbare leegte 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 openbare leegte 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"); Antwoordantwoord = RestAssured.get (uriToLastPage); String uriToNextPage = extractURIByRel (response.getHeader ("Link"), "next"); assertNull (uriToNextPage); }

Merk op dat de volledige low-level code voor extractURIByRel - verantwoordelijk voor het extraheren van de URI's door rel relatie is hier.

7. Alle bronnen ophalen

Over hetzelfde onderwerp van paginering en vindbaarheid, de keuze moet worden gemaakt als een klant alle bronnen in het systeem in één keer mag ophalen, of als de klant er gepagineerd om moet vragen.

Als de keuze wordt gemaakt dat de client niet alle bronnen kan ophalen met een enkel verzoek, en paginering is niet optioneel maar vereist, dan zijn er verschillende opties beschikbaar voor de reactie op een 'Get All'-verzoek. Een optie is om een ​​404 (Niet gevonden) en gebruik de Koppeling koptekst om de eerste pagina vindbaar te maken:

Link =; rel = "eerste",; rel = "laatste"

Een andere optie is om omleiding terug te sturen - 303 (Zie Overige) - naar de eerste pagina. Een meer conservatieve route zou zijn om gewoon een 405 (Methode niet toegestaan) voor het GET-verzoek.

8. REST-oproepen met Bereik HTTP-headers

Een relatief andere manier om paginering te implementeren, is door te werken met de HTTP Bereik headersBereik, Inhoudsbereik, If-bereik, Accepteer-bereiken - en HTTP-statuscodes – 206 (Gedeeltelijke inhoud), 413 (Verzoekentiteit te groot), 416 (Gevraagd bereik niet bevredigend).

Een van de opvattingen over deze benadering is dat de extensies voor het HTTP-bereik niet bedoeld waren voor paginering en dat ze moeten worden beheerd door de server, niet door de toepassing. Het implementeren van paginering op basis van de HTTP Range-header-extensies is niettemin technisch mogelijk, hoewel lang niet zo gebruikelijk als de implementatie die in dit artikel wordt besproken.

9. Spring Data REST Paginering

Als we in Spring Data een paar resultaten uit de volledige dataset moeten retourneren, kunnen we ze gebruiken Pageable repository-methode, omdat het altijd een Bladzijde. De resultaten worden geretourneerd op basis van het paginanummer, het paginaformaat en de sorteerrichting.

Spring Data REST herkent automatisch URL-parameters zoals pagina, grootte, sorteren enz.

Om paging-methoden van elke repository te gebruiken, moeten we uitbreiden PagingAndSortingRepository:

openbare interface SubjectRepository breidt PagingAndSortingRepository {} uit

Als we bellen // localhost: 8080 / onderwerpen Spring voegt automatisch de pagina, grootte, sorteren parametersuggesties met de API:

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

Standaard is het paginaformaat 20, maar we kunnen dit wijzigen door zoiets als // localhost: 8080 / onderwerpen? pagina = 10.

Als we paging in onze eigen aangepaste repository-API willen implementeren, moeten we een extra Pageable parameter en zorg ervoor dat de API een Bladzijde:

@RestResource (pad = "nameContains") openbare pagina findByNameContaining (@Param ("naam") Stringnaam, Pageable p);

Elke keer dat we een aangepaste API toevoegen, is een /zoeken endpoint wordt toegevoegd aan de gegenereerde links. Dus als we bellen // localhost: 8080 / onderwerpen / zoeken we zullen een eindpunt zien dat geschikt is voor paginering:

"findByNameContaining": {"href": "// localhost: 8080 / onderwerpen / zoeken / nameContains {? naam, pagina, grootte, sortering}", "sjabloon": true}

Alle API's die implementeren PagingAndSortingRepository zal een Bladzijde. Als we de lijst met resultaten van het Bladzijde, de Inhoud krijgen() API van Bladzijde biedt de lijst met records die zijn opgehaald als resultaat van de Spring Data REST API.

De code in deze sectie is beschikbaar in het spring-data-rest-project.

10. Converteer een Lijst in een Bladzijde

Laten we aannemen dat we een Pageable object als invoer, maar de informatie die we nodig hebben om op te halen staat in een lijst in plaats van een PagingAndSortingRepository. In deze gevallen kan het nodig zijn converteer een Lijst in een Bladzijde.

Stel je voor dat we een lijst met resultaten van een SOAP-service hebben:

Lijst lijst = getListOfFooFromSoapService ();

We hebben toegang tot de lijst nodig op de specifieke posities die zijn gespecificeerd door de Pageable object naar ons gestuurd. Laten we dus de startindex definiëren:

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

En de eindindex:

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

Als we deze twee op hun plaats hebben, kunnen we een Bladzijde om de lijst met elementen ertussen te verkrijgen:

Paginapagina = nieuwe PageImpl (fooList.subList (begin, einde), pageable, fooList.size ());

Dat is het! We kunnen nu terugkeren bladzijde als een geldig resultaat.

En merk op dat als we ook ondersteuning willen geven bij het sorteren, dat nodig is sorteer de lijst voor de sublijst het.

11. Conclusie

In dit artikel wordt geïllustreerd hoe Pagination in een REST API geïmplementeerd kan worden met Spring, en hoe je Discoverability kunt instellen en testen.

Als je dieper wilt ingaan op paginering op het persistentieniveau, bekijk dan mijn JPA- of Hibernate-pagineringstutorials.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project - dit is een op Maven gebaseerd project, dus het moet gemakkelijk te importeren en uit te voeren zijn zoals het is.

REST onder

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS