REST Pagination pada Musim Bunga

REST Teratas

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS

1. Gambaran keseluruhan

Tutorial ini akan menumpukan pada pelaksanaan penomboran dalam REST API, menggunakan Spring MVC dan Spring Data.

2. Halaman sebagai Sumber vs Halaman sebagai Perwakilan

Soalan pertama ketika merancang penomboran dalam konteks seni bina RESTful adalah sama ada mempertimbangkan halaman itu sebagai Sumber yang sebenar atau hanya Perwakilan Sumber .

Memperlakukan halaman itu sendiri sebagai sumber memperkenalkan pelbagai masalah seperti tidak lagi dapat mengenal pasti sumber antara panggilan secara unik. Ini, ditambah dengan fakta bahawa, dalam lapisan ketekunan, halaman bukanlah entiti yang tepat tetapi pemegang yang dibina apabila perlu, membuat pilihan langsung: halaman adalah bagian dari representasi .

Pertanyaan seterusnya dalam reka bentuk penomboran dalam konteks REST adalah di mana memasukkan maklumat paging :

  • dalam laluan URI: / foo / page / 1
  • pertanyaan URI: / foo? page = 1

Perlu diingat bahawa halaman bukan Sumber , pengekodan maklumat halaman dalam URI bukan lagi pilihan.

Kami akan menggunakan kaedah standard untuk menyelesaikan masalah ini dengan mengekodkan maklumat paging dalam pertanyaan URI.

3. Pengawal

Sekarang, untuk pelaksanaannya - Spring MVC Controller untuk penomboran adalah mudah :

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

Dalam contoh ini, kami menyuntikkan dua parameter pertanyaan, ukuran dan halaman, dalam kaedah Controller melalui @RequestParam.

Sebagai alternatif, kita bisa menggunakan objek Pageable , yang memetakan halaman , ukuran , dan menyusun parameter secara automatik. Sebagai tambahan, entiti PagingAndSortingRepository menyediakan kaedah luar kotak yang menyokong penggunaan Pageable sebagai parameter juga.

Kami juga menyuntik Http Response dan UriComponentsBuilder untuk membantu Discoverability - yang kami putuskan melalui acara khusus. Sekiranya itu bukan matlamat API, anda boleh membuang acara khusus.

Akhirnya - perhatikan bahawa fokus artikel ini hanyalah REST dan lapisan web - untuk masuk ke bahagian akses data penomboran dengan lebih mendalam, anda boleh melihat artikel ini mengenai Pagination with Spring Data.

4. Kebolehtemuan untuk REST Pagination

Dalam ruang lingkup penomboran, memenuhi batasan RATE HATEOAS bermaksud membolehkan klien API menemui halaman berikutnya dan sebelumnya berdasarkan halaman semasa dalam navigasi. Untuk tujuan ini, kita akan menggunakan tajuk HTTP Link , ditambah dengan jenis hubungan pautan " seterusnya ", " prev ", " pertama " dan " terakhir " .

Dalam REST, Kebolehtemuan adalah keprihatinan silang , yang berlaku bukan hanya untuk operasi tertentu tetapi juga untuk jenis operasi. Contohnya, setiap kali Sumber dibuat, URI Sumber tersebut dapat dijumpai oleh pelanggan. Oleh kerana keperluan ini relevan untuk pembuatan SETIAP Sumber, kami akan menanganinya secara berasingan.

Kami akan melepaskan masalah ini dengan menggunakan peristiwa, seperti yang telah kami bincangkan dalam artikel sebelumnya yang memfokuskan pada Kebolehkesanan Perkhidmatan REST. Sekiranya berlaku penomboran, acara - PaginatedResultsRetrievedEvent - dipecat di lapisan pengawal. Kemudian kami akan melaksanakan kebolehtemuan dengan pendengar khusus untuk acara ini.

Ringkasnya, pendengar akan memeriksa sama ada navigasi membenarkan halaman berikutnya , sebelumnya , pertama dan terakhir . Sekiranya berlaku - ia akan menambahkan URI yang relevan pada respons sebagai Header HTTP 'Link' .

Mari pergi selangkah demi selangkah sekarang. The UriComponentsBuilder berlalu dari pengawal hanya mengandungi URL tersebut (tuan rumah, port dan laluan konteks). Oleh itu, kita perlu menambah bahagian yang tinggal:

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

Seterusnya, kami akan menggunakan StringJoiner untuk menggabungkan setiap pautan. Kami akan menggunakan uriBuilder untuk menghasilkan URI. Mari lihat bagaimana kita meneruskan pautan ke halaman seterusnya :

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

Mari lihat logik kaedah konstrukNextPageUri :

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

Kami akan meneruskan yang serupa untuk URI selebihnya yang ingin kami sertakan.

Akhirnya, kami akan menambahkan output sebagai tajuk respons:

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

Perhatikan bahawa, untuk jangka masa pendek, saya hanya memasukkan contoh separa kod dan kod penuh di sini.

5. Uji Memandu Penomboran

Kedua-dua logik utama penomboran dan penemuan dapat diliputi oleh ujian integrasi yang kecil dan berfokus. Seperti dalam artikel sebelumnya, kami akan menggunakan perpustakaan yang dijamin REST untuk menggunakan perkhidmatan REST dan untuk mengesahkan hasilnya.

Ini adalah beberapa contoh ujian integrasi penomboran; untuk suite ujian penuh, lihat projek GitHub (pautan di akhir artikel):

@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. Uji Kebolehpasaran Penagihan Memandu

Testing that pagination is discoverable by a client is relatively straightforward, although there is a lot of ground to cover.

The tests will focus on the position of the current page in navigation and the different URIs that should be discoverable from each position:

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

Note that the full low-level code for extractURIByRel – responsible for extracting the URIs by rel relation is here.

7. Getting All Resources

On the same topic of pagination and discoverability, the 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.

If the choice is made that the client cannot retrieve all Resources with a single request, and pagination is not optional but required, then several options are available for the response to a get all request. One option is to return a 404 (Not Found) and use the Link header to make the first page discoverable:

Link=; rel=”first”, ; rel=”last”

Another option is to return redirect – 303 (See Other) – to the first page. A more conservative route would be to simply return to the client a 405 (Method Not Allowed) for the GET request.

8. REST Paging With Range HTTP Headers

A relatively different way of implementing pagination is to work with the HTTP Range headersRange, Content-Range, If-Range, Accept-Ranges – and HTTP status codes – 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

One view on this approach is that the HTTP Range extensions were not intended for pagination and that they should be managed by the Server, not by the Application. Implementing pagination based on the HTTP Range header extensions is nevertheless technically possible, although not nearly as common as the implementation discussed in this article.

9. Spring Data REST Pagination

In Spring Data, if we need to return a few results from the complete data set, we can use any Pageable repository method, as it will always return a Page. The results will be returned based on the page number, page size, and sorting direction.

Spring Data REST automatically recognizes URL parameters like page, size, sort etc.

To use paging methods of any repository we need to extend PagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository{}

If we call //localhost:8080/subjects Spring automatically adds the page, size, sort parameters suggestions with the API:

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

By default, the page size is 20 but we can change it by calling something like //localhost:8080/subjects?page=10.

If we want to implement paging into our own custom repository API we need to pass an additional Pageable parameter and make sure that API returns a Page:

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

Whenever we add a custom API a /search endpoint gets added to the generated links. So if we call //localhost:8080/subjects/search we will see a pagination capable endpoint:

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

All APIs that implement PagingAndSortingRepository will return a Page. If we need to return the list of the results from the Page, the getContent() API of Page provides the list of records fetched as a result of the Spring Data REST API.

The code in this section is available in the spring-data-rest project.

10. Convert a List into a Page

Let's suppose that we have a Pageable object as input, but the information that we need to retrieve is contained in a list instead of a PagingAndSortingRepository. In these cases, we may need to convert a List into a Page.

For example, imagine that we have a list of results from a SOAP service:

List list = getListOfFooFromSoapService();

We need to access the list in the specific positions specified by the Pageable object sent to us. So, let's define the start index:

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

And the end index:

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

Having these two in place, we can create a Page to obtain the list of elements between them:

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

That's it! We can return now page as a valid result.

And note that if we also want to give support for sorting, we need to sort the list before sub-listing it.

11. Conclusion

This article illustrated how to implement Pagination in a REST API using Spring, and discussed how to set up and test Discoverability.

Sekiranya anda ingin mengetahui secara mendalam tentang penomboran pada tahap kegigihan, lihat tutorial penomboran JPA atau Hibernate saya.

Pelaksanaan semua contoh dan coretan kod ini boleh didapati di projek GitHub - ini adalah projek berasaskan Maven, jadi mudah diimport dan dijalankan sebagaimana adanya.

REST bawah

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS