Paginação REST na primavera

Paginação REST na primavera

1. Visão geral

Este tutorial se concentrará emthe implementation of pagination in a REST API, using Spring MVC and Spring Data.

Leitura adicional:

Paginação com Spring REST e tabela AngularJS

Uma visão abrangente de como implementar uma API simples com paginação com Spring e como consumi-la com AngularJS e UI Grid.

Read more

Paginação JPA

Paginação no JPA - como usar o JQL e a API de critérios para fazer a paginação corretamente.

Read more

Descoberta da API REST e HATEOAS

HATEOAS e descoberta de um serviço REST - conduzido por testes.

Read more

2. Página como recurso vs página como representação

A primeira questão ao projetar a paginação no contexto de uma arquitetura RESTful é se deve considerar opage an actual Resource or just a Representation of Resources.

Tratar a própria página como um recurso apresenta uma série de problemas, como não ser mais capaz de identificar recursos exclusivamente entre chamadas. Isso, aliado ao fato de que, na camada de persistência, a página não é uma entidade própria, mas um suporte que se constrói quando necessário, torna a escolha simples:the page is part of the representation.

A próxima pergunta no design de paginação no contexto de REST éwhere to include the paging information:

  • no caminho URI:/foo/page/1

  • a consulta URI:/foo?page=1

Tendo em mente quea page is not a Resource, codificar as informações da página no URI não é mais uma opção.

Vamos usar a maneira padrão de resolver este problema porencoding the paging information in a URI query.

3. O controlador

Agora, para a implementação -the Spring MVC Controller for pagination is straightforward:

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

Neste exemplo, estamos injetando os dois parâmetros de consulta,sizeepage, no método do controlador via@RequestParam.

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically. Além disso, a entidadePagingAndSortingRepository fornece métodos prontos para uso que também suportam o uso dePageable como parâmetro.

Também estamos injetando a resposta Http e oUriComponentsBuilder para ajudar na descoberta - que estamos desacoplando por meio de um evento personalizado. Se esse não for um objetivo da API, você pode simplesmente remover o evento personalizado.

Finalmente - observe que o foco deste artigo é apenas o REST e a camada da web - para se aprofundar na parte de paginação de acesso a dados, você podecheck out this article sobre Paginação com Spring Data.

4. Detecção para paginação REST

No âmbito da paginação, satisfazer oHATEOAS constraint of REST significa habilitar o cliente da API a descobrir as páginasnexteprevious com base na página atual da navegação. Para isso,we’re going to use the Link HTTP header, coupled with the “next“, “prev“, “first” and “last” link relation types.

Em REST,Discoverability is a cross-cutting concern, aplicável não apenas a operações específicas, mas a tipos de operações. Por exemplo, sempre que um Recurso é criado, o URI desse Recurso deve ser descoberto pelo cliente. Uma vez que este requisito é relevante para a criação de QUALQUER recurso, vamos tratá-lo separadamente.

Vamos separar essas preocupações usando eventos, como discutimos emprevious article focusing on Discoverability de um serviço REST. No caso de paginação, o evento -PaginatedResultsRetrievedEvent - é disparado na camada do controlador. Em seguida, implementaremos a descoberta com um ouvinte personalizado para este evento.

Resumindo, o ouvinte irá verificar se a navegação permite páginasnext,previous,firstelast. Em caso afirmativo, seráadd the relevant URIs to the response as a ‘Link' HTTP Header.

Vamos passo a passo agora. OUriComponentsBuilder passado do controlador contém apenas o URL base (o host, a porta e o caminho do contexto). Portanto, teremos que adicionar as seções restantes:

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

    // ...

}

A seguir, usaremos umStringJoiner para concatenar cada link. UsaremosuriBuilder para gerar os URIs. Vamos ver como procederíamos com o link para a páginanext:

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

Vamos dar uma olhada na lógica do métodoconstructNextPageUri:

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

Vamos proceder da mesma forma para o resto dos URIs que queremos incluir.

Por fim, adicionaremos a saída como um cabeçalho de resposta:

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

Observe que, para resumir, incluí apenas uma amostra de código parcial ethe full code here.

5. Paginação de condução de teste

Tanto a lógica principal da paginação quanto a capacidade de descoberta são cobertas por pequenos testes de integração focados. Como emprevious article, usaremosREST-assured library para consumir o serviço REST e verificar os resultados.

Estes são alguns exemplos de testes de integração de paginação; para uma suíte de testes completa, confira o projeto GitHub (link no final do artigo):

@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. Testar a descoberta da paginação

Testar que a paginação é detectável por um cliente é relativamente simples, embora haja muito caminho a percorrer.

The tests will focus on the position of the current page in navigatione os diferentes URIs que devem ser descobertos em cada posição:

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

Observe que o código de baixo nível completo paraextractURIByRel - responsável por extrair os URIs pela relaçãorelis here.

7. Obtendo todos os recursos

No mesmo tópico de paginação e descoberta,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.

Se for feita a escolha de que o cliente não possa recuperar todos os Recursos com uma única solicitação e a paginação não for opcional, mas necessária, várias opções estarão disponíveis para a resposta a uma solicitação de obter todos. Uma opção é retornar um 404 (Not Found) e usar o cabeçalhoLink para tornar a primeira página detectável:

Outra opção é retornar o redirecionamento - 303(See Other) - para a primeira página. Uma rota mais conservadora seria simplesmente retornar ao cliente um 405 (Method Not Allowed) para a solicitação GET.

8. Paginação REST com cabeçalhos HTTPRange

Uma maneira relativamente diferente de implementar a paginação é trabalhar comHTTP Range headers -Range,Content-Range,If-Range,Accept-Ranges - eHTTP status codes - 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

Uma visão dessa abordagem é que as extensões do intervalo HTTP não foram projetadas para paginação e que devem ser gerenciadas pelo servidor, não pelo aplicativo. A implementação da paginação com base nas extensões de cabeçalho do intervalo HTTP é tecnicamente possível, embora não seja tão comum quanto a implementação discutida neste artigo.

9. Paginação REST de Dados do Spring

No Spring Data, se precisarmos retornar alguns resultados do conjunto de dados completo, podemos usar qualquer método de repositórioPageable, pois ele sempre retornará umPage. Os resultados serão retornados com base na página número, tamanho da página e direção de classificação.

Spring Data REST reconhece automaticamente os parâmetros de URL comopage, size, sort etc.

Para usar métodos de paginação de qualquer repositório, precisamos estenderPagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository{}

Se chamarmoshttp://localhost:8080/subjects Spring adiciona automaticamente as sugestões de parâmetrospage, size, sort com a API:

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

Por padrão, o tamanho da página é 20, mas podemos alterá-lo chamando algo comohttp://localhost:8080/subjects?page=10.

Se quisermos implementar a paginação em nossa API de repositório personalizado, precisamos passar um parâmetroPageable adicional e garantir que a API retorne umPage:

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

Sempre que adicionamos uma API personalizada, um ponto de extremidade/search é adicionado aos links gerados. Portanto, se chamarmoshttp://localhost:8080/subjects/search , swe verá um endpoint capaz de paginação:

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

Todas as APIs que implementamPagingAndSortingRepository retornarão umPage. Se precisarmos retornar a lista dos resultados dePage,, ogetContent() API dePage fornece a lista de registros obtidos como resultado da API REST Spring Data.

O código nesta seção está disponível no projetospring-data-rest.

10. Converta umList em umPage

Vamos supor que temos um objetoPageable como entrada, mas as informações que precisamos recuperar estão contidas em uma lista em vez dePagingAndSortingRepository. Nesses casos, podemos precisar deconvert a List into a Page.

Por exemplo, imagine que temos uma lista de resultados de um serviçoSOAP:

List list = getListOfFooFromSoapService();

Precisamos acessar a lista nas posições específicas especificadas pelo objetoPageable enviado para nós. Então, vamos definir o índice inicial:

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

E o índice final:

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

Tendo esses dois no lugar, podemos criar umPage para obter a lista de elementos entre eles:

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

É isso aí! Podemos retornar agorapage como um resultado válido.

E note que se também quisermos dar suporte parasorting, precisamossort the list before sub-listing isso.

11. Conclusão

Este artigo ilustrou como implementar a paginação em uma API REST usando Spring e discutiu como configurar e testar a capacidade de descoberta.

Se você quiser se aprofundar na paginação no nível de persistência, verifique meus tutoriais de paginaçãoJPA ouHibernate.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.