Pagination REST au printemps

RESTATION Pagination au printemps

1. Vue d'ensemble

Ce tutoriel se concentrera surthe implementation of pagination in a REST API, using Spring MVC and Spring Data.

Lectures complémentaires:

Pagination avec table Spring REST et AngularJS

Un aperçu complet de la manière de mettre en œuvre une API simple avec la pagination avec Spring et de la consommer avec AngularJS et UI Grid.

Read more

Pagination JPA

Pagination dans JPA - comment utiliser JQL et l'API Criteria pour effectuer la pagination correctement.

Read more

Découvrabilité de l'API REST et HATEOAS

HATEOAS et la possibilité de découvrir un service REST - piloté par des tests.

Read more

2. Page en tant que ressource vs page en tant que représentation

La première question lors de la conception de la pagination dans le contexte d'une architecture RESTful est de savoir s'il faut prendre en compte lespage an actual Resource or just a Representation of Resources.

Traiter la page elle-même comme une ressource pose de nombreux problèmes, tels que l'impossibilité d'identifier de manière unique les ressources entre les appels. Ceci, ajouté au fait que, dans la couche de persistance, la page n'est pas une entité propre mais un support qui est construit lorsque cela est nécessaire, rend le choix simple:the page is part of the representation.

La question suivante dans la conception de pagination dans le contexte de REST estwhere to include the paging information:

  • dans le chemin URI:/foo/page/1

  • la requête URI:/foo?page=1

En gardant à l'esprit quea page is not a Resource, l'encodage des informations de page dans l'URI n'est plus une option.

Nous allons utiliser la méthode standard pour résoudre ce problème parencoding the paging information in a URI query.

3. Le controlle

Maintenant, pour l'implémentation -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();
}

Dans cet exemple, nous injectons les deux paramètres de requête,size etpage, dans la méthode Controller via@RequestParam.

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically. De plus, l'entitéPagingAndSortingRepository fournit des méthodes prêtes à l'emploi qui prennent également en charge l'utilisation desPageable comme paramètre.

Nous injectons également à la fois la réponse Http et lesUriComponentsBuilder pour aider à la découvrabilité - que nous découplons via un événement personnalisé. Si ce n'est pas un objectif de l'API, vous pouvez simplement supprimer l'événement personnalisé.

Enfin - notez que cet article se concentre uniquement sur le REST et la couche Web - pour approfondir la partie accès aux données de la pagination, vous pouvezcheck out this article sur la pagination avec Spring Data.

4. Découvrabilité pour la pagination REST

Dans le cadre de la pagination, satisfaire lesHATEOAS constraint of REST signifie permettre au client de l'API de découvrir les pagesnext etprevious en fonction de la page courante dans la navigation. Pour cela,we’re going to use the Link HTTP header, coupled with the “next“, “prev“, “first” and “last” link relation types.

Dans REST,Discoverability is a cross-cutting concern, applicable non seulement à des opérations spécifiques mais à des types d'opérations. Par exemple, chaque fois qu'une ressource est créée, l'URI de cette ressource doit être détectable par le client. Étant donné que cette exigence est pertinente pour la création de TOUTE ressource, nous la traiterons séparément.

Nous allons dissocier ces problèmes à l'aide d'événements, comme nous l'avons vu dans lesprevious article focusing on Discoverability d'un service REST. Dans le cas de la pagination, l'événement -PaginatedResultsRetrievedEvent - est déclenché dans la couche contrôleur. Ensuite, nous mettrons en œuvre la découvrabilité avec un écouteur personnalisé pour cet événement.

En bref, l'auditeur vérifiera si la navigation autorise des pagesnext,previous,first etlast. Si c'est le cas - il seraadd the relevant URIs to the response as a ‘Link' HTTP Header.

Allons-y étape par étape maintenant. LesUriComponentsBuilder transmis par le contrôleur ne contiennent que l'URL de base (l'hôte, le port et le chemin du contexte). Par conséquent, nous devrons ajouter les sections 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 );

    // ...

}

Ensuite, nous utiliserons unStringJoiner pour concaténer chaque lien. Nous utiliserons lesuriBuilder pour générer les URI. Voyons comment nous allons procéder avec le lien vers la pagenext:

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

Jetons un œil à la logique de la méthodeconstructNextPageUri:

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

Nous procéderons de la même manière pour le reste des URI que nous souhaitons inclure.

Enfin, nous ajouterons la sortie en tant qu'en-tête de réponse:

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

Notez que, par souci de concision, j'ai inclus uniquement un échantillon de code partiel etthe full code here.

5. Test de pagination de conduite

La logique principale de la pagination et de la découvrabilité sont couvertes par de petits tests d'intégration ciblés. Comme dans lesprevious article, nous utiliserons lesREST-assured library pour consommer le service REST et vérifier les résultats.

Voici quelques exemples de tests d’intégration de pagination; pour une suite complète de tests, consultez le projet GitHub (lien à la fin de l'article):

@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. Test de découverte de la pagination de conduite

Tester que la pagination peut être découverte par un client est relativement simple, bien qu'il y ait beaucoup de chemin à parcourir.

The tests will focus on the position of the current page in navigation et les différents URI qui devraient être détectables à partir de chaque 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);
}

Notez que le code de bas niveau complet pourextractURIByRel - responsable de l'extraction des URI parrel relationis here.

7. Obtenir toutes les ressources

Sur le même sujet de pagination et de découvrabilité,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.

Si vous choisissez que le client ne puisse pas extraire toutes les ressources avec une seule requête et que la pagination n'est pas facultative, elle est obligatoire. Plusieurs options sont disponibles pour répondre à une requête get all. Une option consiste à renvoyer un 404 (Not Found) et à utiliser l'en-têteLink pour rendre la première page détectable:

Une autre option consiste à renvoyer la redirection - 303(See Other) - vers la première page. Une voie plus prudente consisterait simplement à renvoyer au client un 405 (Method Not Allowed) pour la requête GET.

8. Pagination REST avec les en-têtes HTTPRange

Une manière relativement différente d'implémenter la pagination est de travailler avec lesHTTP Range headers -Range,Content-Range,If-Range,Accept-Ranges - etHTTP status codes - 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

Selon cette approche, les extensions de la plage HTTP n'étaient pas destinées à la pagination et devaient être gérées par le serveur et non par l'application. L'implémentation d'une pagination basée sur les extensions d'en-tête HTTP Range est néanmoins techniquement possible, bien qu'elle ne soit pas aussi commune que l'implémentation décrite dans cet article.

9. Données de printemps REST Pagination

Dans Spring Data, si nous devons retourner quelques résultats de l'ensemble de données complet, nous pouvons utiliser n'importe quelle méthode de référentielPageable, car elle retournera toujours unPage. Les résultats seront retournés en fonction de la page nombre, taille de page et sens de tri.

Spring Data REST reconnaît automatiquement les paramètres d'URL commepage, size, sort etc.

Pour utiliser les méthodes de pagination de n'importe quel référentiel, nous devons étendrePagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository{}

Si nous appelonshttp://localhost:8080/subjects Spring ajoute automatiquement les suggestions de paramètrespage, size, sort avec l'API:

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

Par défaut, la taille de la page est de 20 mais nous pouvons la changer en appelant quelque chose commehttp://localhost:8080/subjects?page=10.

Si nous voulons implémenter la pagination dans notre propre API de référentiel personnalisée, nous devons passer un paramètrePageable supplémentaire et nous assurer que l'API renvoie unPage:

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

Chaque fois que nous ajoutons une API personnalisée, un point de terminaison/searchest ajouté aux liens générés. Donc, si nous appelonshttp://localhost:8080/subjects/search we verra un point de terminaison capable de pagination:

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

Toutes les API qui implémententPagingAndSortingRepository renverront unPage. Si nous devons renvoyer la liste des résultats desPage,, legetContent() API dePage fournit la liste des enregistrements récupérés à la suite de l'API REST Spring Data.

Le code de cette section est disponible dans le projetspring-data-rest.

10. Convertir unList en unPage

Supposons que nous ayons un objetPageable en entrée, mais que les informations que nous devons récupérer sont contenues dans une liste au lieu dePagingAndSortingRepository. Dans ces cas, nous pouvons avoir besoin deconvert a List into a Page.

Par exemple, imaginons que nous ayons une liste de résultats d'un serviceSOAP:

List list = getListOfFooFromSoapService();

Nous devons accéder à la liste dans les positions spécifiques spécifiées par l'objetPageable qui nous est envoyé. Alors, définissons l'index de départ:

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

Et l'indice final:

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

Ayant ces deux en place, nous pouvons créer unPage pour obtenir la liste des éléments entre eux:

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

C'est ça! Nous pouvons renvoyer maintenantpage comme résultat valide.

Et notez que si nous voulons également prendre en chargesorting, nous devons lesort the list before sub-listing.

11. Conclusion

Cet article explique comment implémenter la pagination dans une API REST à l'aide de Spring et explique comment configurer et tester la capacité de découverte.

Si vous souhaitez approfondir la pagination dans le niveau de persistance, consultez mes tutoriels de paginationJPA ouHibernate.

L'implémentation de tous ces exemples et extraits de code se trouve dans leGitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.