OSTALO Paginiranje u proljeće

OSTALO Vrh

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ

1. Pregled

Ovaj vodič bit će usredotočen na implementacija paginacije u REST API, koristeći Spring MVC i Spring Data.

2. Stranica kao resurs vs Stranica kao reprezentacija

Prvo pitanje kod dizajniranja paginacije u kontekstu RESTful arhitekture je treba li uzeti u obzir stranice stvarni resurs ili samo prikaz resursa.

Tretiranje same stranice kao resursa uvodi mnoštvo problema kao što je nemogućnost jedinstvenog prepoznavanja resursa između poziva. To, zajedno s činjenicom da stranica u sloju postojanosti nije odgovarajući entitet, već je nositelj koji se izrađuje po potrebi, odabir čini jednostavnim: stranica je dio predstavljanja.

Sljedeće pitanje u dizajnu paginacije u kontekstu REST-a je gdje uključiti informacije o straničenju:

  • na URI putu: / foo / page / 1
  • URI upit: / foo? page = 1

Imajući to na umu stranica nije Resurs, kodiranje podataka o stranici u URI-u više nije opcija.

Upotrijebit ćemo standardni način rješavanja ovog problema do kodiranje podataka o straničenju u URI upitu.

3. Upravljač

Sada, za provedbu - proljetni MVC kontroler za paginaciju je jednostavan:

@GetMapping (params = {"page", "size"}) javni popis findPaginated (@RequestParam ("page") int page, @RequestParam ("size") int size, UriComponentsBuilder uriBuilder, HttpServletResponse response) {Page resultPage = service .findPaginated (stranica, veličina); if (page> resultPage.getTotalPages ()) {throw new MyResourceNotFoundException (); } eventPublisher.publishEvent (novi PaginatedResultsRetrievedEvent (Foo.class, uriBuilder, odgovor, stranica, resultPage.getTotalPages (), veličina)); vratiti resultPage.getContent (); }

U ovom primjeru ubrizgavamo dva parametra upita, veličina i stranica, u metodi Controller putem @RequestParam.

Alternativno, mogli smo koristiti i Pageable objekt koji mapira stranica, veličina, i vrsta parametara automatski. Osim toga, PagingAndSortingRepository entitet nudi gotove metode koje podržavaju upotrebu Pageable kao parametar također.

Također ubrizgavamo i Http odgovor i UriComponentsBuilder za pomoć u otkrivanju - koje razdvajamo putem prilagođenog događaja. Ako to nije cilj API-ja, možete jednostavno ukloniti prilagođeni događaj.

Konačno - imajte na umu da je fokus ovog članka samo REST i web sloj - da biste dublje ušli u dio paginacije za pristup podacima, možete pogledati ovaj članak o Paginaciji s proljetnim podacima.

4. Otkrivenost za REST paginaciju

Unutar domena paginacije, zadovoljavajući HATEOAS ograničenje ODMORA znači omogućavanje klijentu API-ja da otkrije Sljedeći i prethodni stranice temeljene na trenutnoj stranici u navigaciji. Za ovu svrhu, koristit ćemo Veza HTTP zaglavlje, zajedno s "Sljedeći“, “prev“, “prvi"I"posljednji”Tipovi veza veza.

U ODMORU, Otkrivenost je zajednička briga, primjenjiv ne samo na određene operacije već i na vrste operacija. Na primjer, svaki put kad se kreira resurs, klijent bi trebao otkriti URI tog resursa. Budući da je ovaj zahtjev relevantan za stvaranje BILO KOJIH resursa, obradit ćemo ga zasebno.

Te ćemo nedoumice razdvojiti pomoću događaja, kao što smo govorili u prethodnom članku, usredotočujući se na otkrivanje REST usluge. U slučaju paginacije, događaj - PaginatedResultsRetrievedEvent - otpušta se u sloju kontrolera. Tada ćemo primijeniti mogućnost otkrivanja pomoću prilagođenog preslušača za ovaj događaj.

Ukratko, slušatelj će provjeriti dopušta li navigacija a Sljedeći, prethodni, prvi i posljednji stranice. Ako se dogodi - hoće dodajte relevantne URI-je u odgovor kao HTTP zaglavlje "Link".

Idemo sada korak po korak. The UriComponentsBuilder proslijeđen iz kontrolera sadrži samo osnovni URL (host, port i staza konteksta). Stoga ćemo morati dodati preostale odjeljke:

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

Dalje ćemo koristiti StringJoiner za spajanje svake poveznice. Koristit ćemo uriBuilder za generiranje URI-ja. Pogledajmo kako bismo nastavili s vezom na Sljedeći stranica:

StringJoiner linkHeader = novi StringJoiner (","); if (hasNextPage (stranica, totalPages)) {String uriForNextPage = constructNextPageUri (uriBuilder, stranica, veličina); linkHeader.add (createLinkHeader (uriForNextPage, "next")); }

Pogledajmo logiku constructNextPageUri metoda:

String constructNextPageUri (UriComponentsBuilder uriBuilder, int stranica, int veličina) {return uriBuilder.replaceQueryParam (STRANICA, stranica + 1) .replaceQueryParam ("size", size) .build () .encode () .toUriString (); }

Slijedit ćemo i za ostale URI-je koje želimo uključiti.

Na kraju ćemo dodati izlaz kao zaglavlje odgovora:

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

Imajte na umu da sam radi kratkoće ovdje uključio samo djelomični uzorak koda i puni kod.

5. Isprobajte paginaciju vožnje

I glavna logika paginacije i otkrivanja pokriveni su malim fokusiranim integracijskim testovima. Kao i u prethodnom članku, koristit ćemo REST-osiguranu knjižnicu za konzumiranje REST usluge i za provjeru rezultata.

Ovo je nekoliko primjera testova integracije paginacija; za cjeloviti testni paket pogledajte projekt GitHub (veza na kraju članka):

@Test public void whenResourcesAreRetrievedPaged_then200IsReceived () {Response response = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertThat (response.getStatusCode (), je (200)); } @Test public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived () {String url = getFooURL () + "? Page =" + randomNumeric (5) + "& size = 2"; Odgovor odgovora = RestAssured.get.get (url); assertThat (response.getStatusCode (), je (404)); } @Test javna praznina givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources () {createResource (); Odgovor odgovora = RestAssured.get (paths.getFooURL () + "? Page = 0 & size = 2"); assertFalse (response.body (). as (List.class) .isEmpty ()); }

6. Ispitajte mogućnost otkrivanja paginacije vožnje

Testiranje da klijent može otkriti paginaciju relativno je jednostavno, iako postoji mnogo podloga za pokriti.

Testovi će se usredotočiti na položaj trenutne stranice u navigaciji i različite URI-je koji bi se mogli otkriti sa svakog položaja:

@Test public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext () {Response response = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Niz 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"); Niz uriToPrevPage = extractURIByRel (response.getHeader ("Link"), "prev"); assertNull (uriToPrevPage); } @Test public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious () {Response response = RestAssured.get (getFooURL () + "? Page = 1 & size = 2"); Niz uriToPrevPage = extractURIByRel (response.getHeader ("Link"), "prev"); assertEquals (getFooURL () + "? page = 0 & size = 2", uriToPrevPage); } @Test public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable () {Prvi odgovor = RestAssured.get (getFooURL () + "? Page = 0 & size = 2"); Niz uriToLastPage = extractURIByRel (first.getHeader ("Link"), "last"); Odgovor odgovora = RestAssured.get (uriToLastPage); Niz uriToNextPage = extractURIByRel (response.getHeader ("Link"), "next"); assertNull (uriToNextPage); }

Imajte na umu da puni kod niske razine za ekstraktURIByRel - odgovoran za izdvajanje URI-ja rel odnos je ovdje.

7. Dobivanje svih resursa

Na istu temu paginacije i otkrivanja, izbor mora biti napravljen ako je klijentu dopušteno istovremeno dohvatiti sve resurse u sustavu ili ako klijent mora tražiti da su paginirani.

Ako se odluči da klijent ne može dohvatiti sve resurse s jednim zahtjevom, a paginacija nije neobavezna, ali je potrebna, tada je dostupno nekoliko opcija za odgovor na zahtjev za dobivanje svih zahtjeva. Jedna od mogućnosti je vratiti 404 (Nije pronađeno) i koristite Veza zaglavlje kako bi prva stranica bila vidljiva:

Veza =; rel = "prvi",; rel = "zadnji"

Druga je mogućnost vratiti preusmjeravanje - 303 (Vidi Ostalo) - na prvu stranicu. Konzervativniji način bio bi jednostavno vratiti klijentu 405 (Metoda nije dopuštena) za GET zahtjev.

8. OSTALO Paging sa Domet HTTP zaglavlja

Relativno drugačiji način primjene paginacije je rad s HTTP Domet zaglavljaDomet, Sadržaj-raspon, Ako-domet, Raspoloživi rasponi - i HTTP kodovi statusa – 206 (Djelomični sadržaj), 413 (Zatražite entitet prevelik), 416 (Zatraženi raspon nije zadovoljavajući).

Jedno od stajališta ovog pristupa je da proširenja HTTP raspona nisu bila namijenjena paginaciji i da bi njima trebao upravljati poslužitelj, a ne aplikacija. Implementacija paginacije na temelju proširenja zaglavlja HTTP raspona ipak je tehnički moguća, iako ni približno toliko česta kao implementacija o kojoj se govori u ovom članku.

9. Proljetna podataka REST Paginacija

Ako trebamo vratiti nekoliko rezultata iz cjelovitog skupa podataka u Spring Data, možemo koristiti bilo koji Pageable metoda spremišta, jer će uvijek vratiti a Stranica. Rezultati će se vratiti na temelju broja stranice, veličine stranice i smjera sortiranja.

Spring Data REST automatski prepoznaje URL parametre poput stranica, veličina, sortiranje itd.

Da bismo koristili metode straničenja bilo kojeg spremišta, moramo ga proširiti PagingAndSortingRepository:

javno sučelje SubjectRepository proširuje PagingAndSortingRepository {}

Ako nazovemo // localhost: 8080 / subjekti Proljeće automatski dodaje stranica, veličina, sortiranje prijedlozi parametara s API-jem:

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

Prema zadanim postavkama veličina stranice je 20, ali možemo je promijeniti pozivom na neki način // localhost: 8080 / subjekti? stranica = 10.

Ako želimo implementirati straničenje u naš vlastiti API prilagođenog spremišta, moramo proslijediti dodatni Pageable parametar i pobrinite se da API vrati a Stranica:

@RestResource (path = "nameContains") javna stranica findByNameContaining (@Param ("name") Naziv niza, stranica sa str);

Kad god dodamo prilagođeni API a /traži krajnja točka dodaje se na generirane veze. Pa ako nazovemo // localhost: 8080 / subjekti / pretraživanje vidjet ćemo paginacijsku krajnju točku:

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

Svi API-ji koji implementiraju PagingAndSortingRepository vratit će a Stranica. Ako trebamo vratiti popis rezultata iz Stranica, the getContent () API od Stranica pruža popis zapisa preuzetih kao rezultat Spring Data REST API-ja.

Kôd u ovom odjeljku dostupan je u projektu spring-data-rest.

10. Pretvori a Popis u a Stranica

Pretpostavimo da imamo a Pageable objekt kao ulaz, ali informacije koje moramo dohvatiti sadrže se na popisu umjesto u PagingAndSortingRepository. U tim ćemo slučajevima možda trebati pretvoriti a Popis u a Stranica.

Na primjer, zamislimo da imamo popis rezultata SOAP usluge:

Lista popisa = getListOfFooFromSoapService ();

Moramo pristupiti popisu na određenim pozicijama koje navodi Pageable objekt poslan nama. Pa, definirajmo indeks početka:

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

I krajnji indeks:

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

Imajući ovo dvoje na mjestu, možemo stvoriti Stranica da biste dobili popis elemenata između njih:

Stranica stranice = new PageImpl (fooList.subList (početak, kraj), pageable, fooList.size ());

To je to! Sad se možemo vratiti stranica kao valjani rezultat.

I imajte na umu da ako također želimo pružiti podršku za sortiranje, moramo sortirajte popis prije podlista to.

11. Zaključak

Ovaj je članak ilustrirao kako implementirati paginaciju u REST API pomoću Springa i raspravljao o tome kako postaviti i testirati mogućnost otkrivanja.

Ako želite dublje proučiti paginaciju na razini postojanosti, pogledajte moje vodiče za paginaciju JPA ili Hibernate.

Provedbu svih ovih primjera i isječaka koda možete pronaći u projektu GitHub - ovo je projekt zasnovan na Mavenu, pa bi ga trebalo lako uvesti i pokrenuti kakav jest.

OSTALO dno

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ