Paginasi REST di Musim Semi

REST Top

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

>> LIHAT KURSUSnya

1. Ikhtisar

Tutorial ini akan fokus pada implementasi pagination di REST API, menggunakan Spring MVC dan Spring Data.

2. Halaman sebagai Sumber dan Halaman sebagai Representasi

Pertanyaan pertama saat mendesain pagination dalam konteks arsitektur RESTful adalah apakah akan menganggap halaman sebagai Resource sebenarnya atau hanya Representasi Resource .

Memperlakukan halaman itu sendiri sebagai sumber daya akan menimbulkan sejumlah masalah seperti tidak lagi dapat mengidentifikasi sumber daya secara unik di antara panggilan. Hal ini, ditambah dengan fakta bahwa, di lapisan persistensi, halaman bukanlah entitas yang tepat, tetapi penahan yang dibuat saat diperlukan, membuat pilihan langsung: halaman adalah bagian dari representasi .

Pertanyaan berikutnya dalam desain pagination dalam konteks REST adalah di mana harus menyertakan informasi paging :

  • di jalur URI: / foo / page / 1
  • kueri URI: / foo? page = 1

Perlu diingat bahwa halaman bukanlah Resource , encoding informasi halaman di URI tidak lagi menjadi pilihan.

Kami akan menggunakan cara standar untuk memecahkan masalah ini dengan mengenkode informasi paging dalam kueri URI.

3. Pengendali

Sekarang, untuk implementasinya - Spring MVC Controller untuk pagination sangatlah 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 memasukkan dua parameter kueri, ukuran dan halaman, dalam metode Pengontrol melalui @RequestParam.

Alternatifnya, kita bisa menggunakan objek Pageable , yang memetakan halaman , ukuran , dan parameter sortir secara otomatis. Selain itu, entitas PagingAndSortingRepository menyediakan metode out-of-the-box yang juga mendukung penggunaan Pageable sebagai parameter.

Kami juga memasukkan Respons Http dan UriComponentsBuilder untuk membantu Discoverability - yang kami pisahkan melalui acara khusus. Jika itu bukan tujuan API, Anda cukup menghapus peristiwa khusus tersebut.

Terakhir - perhatikan bahwa fokus artikel ini hanya REST dan lapisan web - untuk masuk lebih dalam ke bagian akses data penomoran halaman, Anda dapat melihat artikel ini tentang Penomoran halaman dengan Data Musim Semi.

4. Dapat ditemukan untuk Paginasi REST

Dalam lingkup pagination, memenuhi batasan HATEOAS dari REST berarti memungkinkan klien API untuk menemukan halaman berikutnya dan sebelumnya berdasarkan halaman saat ini di navigasi. Untuk tujuan ini, kita akan menggunakan header HTTP Link , digabungkan dengan jenis relasi link " next ", " prev ", " first " dan " last " .

Di REST, Discoverability adalah perhatian lintas sektor , berlaku tidak hanya untuk operasi tertentu tetapi juga untuk jenis operasi. Misalnya, setiap kali Resource dibuat, URI Resource tersebut harus dapat ditemukan oleh klien. Karena persyaratan ini relevan untuk pembuatan Sumber Daya APA PUN, kami akan menanganinya secara terpisah.

Kami akan memisahkan masalah ini menggunakan peristiwa, seperti yang telah kita bahas di artikel sebelumnya yang berfokus pada Dapat ditemukannya Layanan REST. Dalam kasus pagination, event - PaginatedResultsRetrievedEvent - dijalankan di lapisan pengontrol. Kemudian kami akan menerapkan kemampuan untuk dapat ditemukan dengan pendengar khusus untuk acara ini.

Singkatnya, pendengar akan memeriksa apakah navigasi memungkinkan untuk halaman berikutnya , sebelumnya , pertama dan terakhir . Jika ya - ini akan menambahkan URI yang relevan ke respons sebagai Header HTTP 'Tautan' .

Mari selangkah demi selangkah sekarang. The UriComponentsBuilder lulus dari controller hanya berisi URL dasar (host, port dan jalur konteks). Oleh karena itu, kami harus menambahkan bagian yang tersisa:

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

Selanjutnya, kami akan menggunakan StringJoiner untuk menggabungkan setiap tautan. Kami akan menggunakan uriBuilder untuk menghasilkan URI. Mari kita lihat bagaimana kita akan melanjutkan dengan tautan ke halaman berikutnya :

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

Mari kita lihat logika metode constructNextPageUri :

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

Kami akan melanjutkan serupa untuk URI lainnya yang ingin kami sertakan.

Terakhir, kami akan menambahkan output sebagai header respons:

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

Perhatikan bahwa, untuk singkatnya, saya hanya menyertakan sampel kode parsial dan kode lengkap di sini.

5. Uji Paginasi Mengemudi

Baik logika utama pagination dan kemampuan untuk dapat ditemukan dicakup oleh tes integrasi kecil yang terfokus. Seperti di artikel sebelumnya, kita akan menggunakan pustaka yang dijamin REST untuk menggunakan layanan REST dan untuk memverifikasi hasilnya.

Ini adalah beberapa contoh pengujian integrasi pagination; untuk rangkaian pengujian lengkap, lihat proyek GitHub (tautan di bagian 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. Tes Mengemudi Pagination Dapat Ditemukan

Menguji bahwa pagination dapat ditemukan oleh klien relatif mudah, meskipun ada banyak hal yang harus dibahas.

Pengujian akan berfokus pada posisi halaman saat ini dalam navigasi dan URI berbeda yang harus dapat ditemukan dari setiap posisi:

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

Perhatikan bahwa kode tingkat rendah lengkap untuk extractURIByRel - bertanggung jawab untuk mengekstrak URI oleh relasi rel ada di sini.

7. Mendapatkan Semua Sumber Daya

Pada topik pagination dan kemampuan untuk ditemukan yang sama, pilihan harus dibuat jika klien diizinkan untuk mengambil semua Resource di sistem sekaligus, atau jika klien harus memintanya untuk diberi nomor halaman .

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.

Jika Anda ingin mempelajari pagination secara mendalam di tingkat persistensi, lihat tutorial pagination JPA atau Hibernate saya.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub - ini adalah proyek berbasis Maven, jadi semestinya mudah untuk mengimpor dan menjalankannya apa adanya.

REST bawah

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

>> LIHAT KURSUSnya