Pagination dans une collection de repos

134

Je suis intéressé à exposer une interface REST directe à des collections de documents JSON (pensez CouchDB ou Persevere ). Le problème que je rencontre est de savoir comment gérer l' GETopération sur la racine de la collection si la collection est grande.

À titre d'exemple, prétendez que j'expose la Questionstable de StackOverflow où chaque ligne est exposée en tant que document (non pas qu'il existe nécessairement une telle table, juste un exemple concret d'une collection importante de «documents»). La collection sera mis à disposition à /db/questionsla api habituelle CRUD GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsest en jeu. Le moyen standard d'obtenir toute la collection est de le faire, GET /db/questionsmais si cela vide naïvement chaque ligne en tant qu'objet JSON, vous obtiendrez un téléchargement assez important et beaucoup de travail de la part du serveur.

La solution est, bien sûr, la pagination. Dojo a résolu ce problème dans son JsonRestStore via une extension intelligente conforme à la RFC2616 d'utilisation de l'en- Rangetête avec une unité de plage personnalisée items. Le résultat est un 206 Partial Contentqui renvoie uniquement la plage demandée. L'avantage de cette approche par rapport à un paramètre de requête est qu'elle laisse la chaîne de requête pour ... requêtes (par exemple GET /db/questions/?score>200ou une partie, et oui qui serait encodée %3E).

Cette approche couvre complètement le comportement que je souhaite. Le problème est que la RFC 2616 spécifie que sur une réponse 206 (c'est moi qui souligne):

La demande DOIT avoir inclus un champ d'en-tête Range ( section 14.35 ) indiquant la gamme souhaitée, et PEUT avoir inclus un champ d'en-tête If-Range ( section 14.27 ) pour rendre la demande conditionnelle.

Cela a du sens dans le contexte de l'utilisation standard de l'en-tête, mais c'est un problème car j'aimerais que la réponse 206 soit la réponse par défaut pour gérer les clients naïfs / les personnes aléatoires explorant.

J'ai parcouru la RFC en détail à la recherche d'une solution, mais je n'ai pas été satisfait de mes solutions et je suis intéressé par la prise en charge du problème par les SO.

Idées que j'ai eues:

  • Revenez 200avec un en- Content-Rangetête! - Je ne pense pas que ce soit faux, mais je préférerais un indicateur plus évident que la réponse n'est qu'un contenu partiel.
  • Retour400 Range Required - Il n'y a pas de code de réponse 400 spécial pour les en-têtes requis, donc l'erreur par défaut doit être utilisée et lue à la main. Cela rend également l'exploration via un navigateur Web (ou un autre client comme Resty) plus difficile.
  • Utilisez un paramètre de requête - L'approche standard, mais j'espère autoriser les requêtes à la persévérance et cela coupe dans l'espace de noms de la requête.
  • Revenez juste 206! - Je pense que la plupart des clients ne paniqueraient pas, mais je préfère ne pas aller à l'encontre d'un MUST dans la RFC
  • Prolongez les spécifications! Retour266 Partial Content - Se comporte exactement comme 206 mais est en réponse à une demande qui NE DOIT PAS contenir l'en- Rangetête. Je pense que 266 est suffisamment élevé pour que je ne devrais pas rencontrer de problèmes de collision et cela a du sens pour moi, mais je ne sais pas si cela est considéré comme tabou ou non.

Je pense que c'est un problème assez courant et j'aimerais que cela se fasse d'une manière de facto afin que moi ou quelqu'un d'autre ne réinvente pas la roue.

Quelle est la meilleure façon d'exposer une collection complète via HTTP lorsque la collection est volumineuse?

Karl Guertin
la source
21
Wow, c'est un bon exemple de question sur laquelle une réflexion sérieuse a déjà été menée.
Heiko Rupp
duplication possible de la Pagination dans une application web REST
rds
1
En ce qui concerne l'approche de Dojo dans l'utilisation de l'en-tête Range, bien que Accept-Ranges autorise l'extension, de tout ce que je peux dire, l'EBNF pour Range ne le fait pas: tools.ietf.org/html/rfc2616#section-14.35.2 . La spécification indique Range = "Range" ":" ranges-specifieroù ce dernier dans tools.ietf.org/html/rfc2616#section-14.35.1 est simplement décrit comme "byte-range-specifier" qui doit commencer par "bytes-unit" qui est défini comme la chaîne "bytes ".
Brett Zamir
2
L'en- Content-Rangetête s'applique au corps (peut être utilisé avec une requête lors du téléchargement de gros fichiers, etc., ou pour une réponse lors du téléchargement). L'en- Rangetête est utilisé pour demander une certaine plage. Il faut répondre 206quand l'en- Rangetête a été inclus dans la demande. Si ce n'est pas le cas, la réponse peut toujours inclure un en- Content-Rangetête, mais le code de réponse doit l'être 200. Cet en-tête semble en fait idéal pour la pagination.
Stijn de Witt
Mais la RFC 2616 elle-même dit que "les implémentations HTTP / 1.1 PEUVENT ignorer les plages spécifiées en utilisant d'autres unités." Est-ce donc une bonne pratique d'utiliser les en-têtes Range pour la pagination? car cela pourrait compromettre l'interopérabilité.
chetan choulwar

Réponses:

23

Mon instinct est que les extensions de plage HTTP ne sont pas conçues pour votre cas d'utilisation et que vous ne devriez donc pas essayer. Une réponse partielle implique 206et 206ne doit être envoyée que si le client l'a demandé.

Vous voudrez peut-être envisager une approche différente, telle que celle utilisée dans Atom (où la représentation par conception peut être partielle et est renvoyée avec un statut 200et potentiellement des liens de pagination). Voir RFC 4287 et RFC 5005 .

Julian Reschke
la source
14
L'utilisation du Dojo est entièrement conforme aux spécifications. Si le serveur ne comprend pas l' itemsunité de plage, il renvoie une réponse complète. Je connais Atom mais ce n'est pas la solution générale à la pagination Rest. Ce n'est pas une solution pour un seul cas, mais plutôt ce que devrait être la solution générale. Tous les documents / collections ne correspondent pas au modèle Atom et il n'y a aucune raison de le forcer sauf si nécessaire.
Karl Guertin
1
@KarlGuertin D'accord. Dommage que ce soit la réponse acceptée, car il semble que beaucoup dans la communauté adoptent réellement Rangeet Content-Rangeà des fins de pagination.
Stijn de Witt
34

Je ne suis pas vraiment d'accord avec certains d'entre vous. Je travaille depuis des semaines sur ces fonctionnalités pour mon service REST. Ce que j'ai fini par faire est vraiment simple. Ma solution n'a de sens que pour ce que les gens de REST appellent une collection.

Le client DOIT inclure un en-tête "Range" pour indiquer la partie de la collection dont il a besoin, ou être prêt à gérer une erreur 413 REQUESTED ENTITY TOO LARGE lorsque la collection demandée est trop grande pour être récupérée en un seul aller-retour.

Le serveur envoie une réponse 206 PARTIAL CONTENT, avec l'en-tête Content-Range spécifiant quelle partie de la ressource a été envoyée, et un en-tête ETag pour identifier la version actuelle de la collection. J'utilise généralement un ETag {last_modification_timestamp} - {resource_id} de type Facebook, et je considère que l'ETag d'une collection est celui de la ressource la plus récemment modifiée qu'elle contient.

Pour demander une partie spécifique d'une collection, le client DOIT utiliser l'en-tête "Range", et remplir l'en-tête "If-Match" avec l'ETag de la collection obtenue à partir de demandes précédemment effectuées pour acquérir d'autres parties de la même collection. Le serveur peut donc vérifier que la collection n'a pas changé avant d'envoyer la partie demandée. Si une version plus récente existe, une réponse 412 PRECONDITION FAILED est renvoyée pour inviter le client à récupérer la collection à partir de zéro. Cela est nécessaire car cela peut signifier que certaines ressources peuvent avoir été ajoutées ou supprimées avant ou après la partie actuellement demandée.

J'utilise ETag / If-Match en tandem avec Last-Modified / If-Unmodified-Since pour optimiser le cache. Les navigateurs et les mandataires peuvent s'appuyer sur l'un d'entre eux ou sur les deux pour leurs algorithmes de mise en cache.

Je pense qu'une URL devrait être propre à moins qu'elle n'inclue une requête de recherche / filtre. Si vous y réfléchissez, une recherche n'est rien de plus qu'une vue partielle d'une collection. Au lieu des URL de type voitures / recherche? Q = BMW, nous devrions voir plus de voitures? Fabricant = BMW.

Mohamed
la source
Vouliez-vous dire 416 «Requested Range Not Satisfiable» ou «413» Request Entity Too Large?
1
@Mohamed Je pense que vous voulez dire If-Unmodified-Since, ce qui correspond à la variante E-Tag If-Match, plutôt que If-Modified-Since. Cela dit, vous pouvez également envisager de supprimer cette contrainte, en fonction de votre cas d'utilisation. Supposons que vous ayez une collection qui ne grandit qu'à partir du haut (comme certaines collections de style "la plus récente en premier"), le pire qui puisse arriver si cette collection change entre les demandes est qu'un utilisateur qui parcourt une collection voit les entrées deux fois. (Ce qui en soi est également une information utile: il indique à l'utilisateur que la collection a changé)
Eugene Beresovsky
20
413 est "Request Entity Too Large", et non "Requested Entity Too Large". Cela signifie que la taille de votre demande, par exemple lors du téléchargement d'un fichier, est supérieure à ce que le serveur est prêt à traiter. Donc, l'utiliser pour cela ne semble pas tout à fait approprié.
user247702
@Mohamed Je sais que c'est une vieille question, mais si l'ETag d'une collection est l'ETag de la ressource la plus récemment modifiée que la collection contient, quelle valeur de l'en-tête If-Match doit être utilisée lors de la modification d'une ressource dans la collection? L'utilisation de la valeur de l'ETag renvoyée avec la collection est erronée car le client pourrait modifier la ressource même s'il ne voit pas le dernier état de la ressource.
Mickael Marrache
8
Je ne suis pas du tout d'accord sur l'utilisation 413. Il s'agit d'un code d'erreur signifiant que le client envoie quelque chose que le serveur refuse d'accepter en raison de sa taille. Pas l'inverse! Voir tools.ietf.org/html/rfc7231#section-6.5.11 (notez que cela dit charge utile de demande . Pas de charge utile de réponse )!
exhuma
7

Vous pouvez toujours revenir Accept-Rangeset Content-Rangesavec un 200code de réponse. Ces deux en-têtes de réponse vous donnent suffisamment d'informations pour déduire les mêmes informations qu'un 206code de réponse fournit explicitement.

J'utiliserais Rangepour la pagination et lui demanderais simplement de renvoyer un 200pour une plaine GET.

Cela semble 100% REST et ne rend pas la navigation plus difficile.

Edit: j'ai écrit un article de blog à ce sujet: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

John Gietzen
la source
5

S'il y a plus d'une page de réponses et que vous ne souhaitez pas proposer l'ensemble de la collection en même temps, cela signifie-t-il qu'il y a plusieurs choix?

Sur une demande à /db/questions, retournez 300 Multiple Choicesavec des Linken-têtes qui spécifient comment accéder à chaque page ainsi qu'un objet JSON ou une page HTML avec une liste d'URL.

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

Vous auriez un en- Linktête pour chaque page de résultats (une chaîne vide signifie l'URL actuelle, et l'URL est la même pour chaque page, juste accessible avec différentes plages), et la relation est définie comme uneLink relation personnalisée selon la spécification à venir . Cette relation expliquerait votre coutume 266ou votre violation de 206. Ces en-têtes sont votre version lisible par machine, car tous vos exemples nécessitent de toute façon un client compréhensif.

(Si vous vous en tenez à la route "range", je crois que votre propre 2xxcode de retour, tel que vous l'avez décrit, serait le meilleur comportement ici. Vous êtes censé le faire pour vos applications et ces ["codes d'état HTTP sont extensibles. "], et vous avez de bonnes raisons.)

300 Multiple Choicesdit que vous DEVRIEZ également fournir à un organisme un moyen pour l'agent utilisateur de choisir. Si votre client est compréhensif, il doit utiliser les en- Linktêtes. Si c'est un utilisateur qui navigue manuellement, peut-être une page HTML avec des liens vers une ressource racine spéciale "paginée" qui peut gérer le rendu de cette page particulière en fonction de l'URL? /humanpage/1/db/questionsou quelque chose de hideux comme ça?


Les commentaires sur le post de Richard Levasseur me rappellent une option supplémentaire: l'en- Accepttête (section 14.1). À l'époque où la spécification oEmbed est sortie, je me suis demandé pourquoi cela n'avait pas été entièrement fait en utilisant HTTP, et j'ai écrit une alternative en les utilisant.

Gardez le 300 Multiple Choices, les en- Linktêtes et la page HTML pour un HTTP naïf initial GET, mais plutôt que d'utiliser des plages, faites en sorte que votre nouvelle relation de pagination définisse l'utilisation de l'en- Accepttête. Votre requête HTTP suivante pourrait ressembler à ceci:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

L'en- Accepttête vous permet de définir un type de contenu acceptable (votre retour JSON), ainsi que des paramètres extensibles pour ce type (votre numéro de page). En reprenant mes notes de mon écriture oEmbed (je ne peux pas le lier ici, je vais le lister dans mon profil), vous pourriez être très explicite et fournir une version de spécification / relation ici au cas où vous auriez besoin de redéfinir ce que pagesignifie le paramètre A l'avenir.

Vitorio
la source
1
+1 en-têtes de lien, mais je recommanderais également les premiers, précédents, suivants, derniers rels communs, ainsi que les archives prev-archive, next-archive et current de la RFC5005.
Joseph Holsten
> Sur une demande à / db / questions, retournez 300 choix multiples avec des en-têtes de lien qui spécifient comment accéder à chaque page [..] Le problème avec cela (et avec la plupart des conceptions REST pures) est que cela tue pour la latence. L'objectif est de minimiser les demandes réseau. Cette première demande devrait donner des résultats, pas des liens vers d'autres demandes qui finiront par donner les données dont nous avons besoin.
Stijn de Witt
4

Éditer:

Après y avoir réfléchi un peu plus, je suis enclin à convenir que les en-têtes Range ne sont pas appropriés pour la pagination. La logique étant, l'en-tête Range est destiné à la réponse du serveur, pas aux applications. Si vous avez fourni 100 mégaoctets de résultats, mais que le serveur (ou client) ne pouvait traiter que 1 mégaoctet à la fois, eh bien, c'est à cela que sert l'en-tête Range.

Je suis également d'avis qu'un sous-ensemble de ressources est sa propre ressource (similaire à l'algèbre relationnelle), donc il mérite d'être représenté dans l'URL.

Donc, fondamentalement, je renonce à ma réponse originale (ci-dessous) sur l'utilisation d'un en-tête.


Je pense que vous avez répondu à votre propre question, plus ou moins - renvoyez 200 ou 206 avec content-range et utilisez éventuellement un paramètre de requête. Je reniflerais l'agent utilisateur et le type de contenu et, en fonction de ceux-ci, vérifierais un paramètre de requête. Sinon, exigez les en-têtes de plage.

Vous avez essentiellement des objectifs contradictoires - laissez les gens utiliser leur navigateur pour explorer (ce qui n'autorise pas facilement les en-têtes personnalisés), ou forcez les gens à utiliser un client spécial qui peut définir des en-têtes (ce qui ne les laisse pas explorer).

Vous pouvez simplement leur fournir le client spécial en fonction de la demande - s'il ressemble à un navigateur ordinaire, envoyez une petite application ajax qui rend la page et définit les en-têtes nécessaires.

Bien sûr, il y a aussi le débat sur la question de savoir si l'URL doit contenir tout l'état nécessaire pour ce genre de chose. Spécifier la plage en utilisant des en-têtes peut être considéré comme "non reposant" par certains.

En passant, ce serait bien si les serveurs pouvaient répondre avec un en-tête «Can-Specify: Header1, header2» et que les navigateurs Web présentent une interface utilisateur afin que les utilisateurs puissent remplir des valeurs, s'ils le souhaitent.

Richard Levasseur
la source
Merci pour la réponse. J'ai réfléchi au sujet, mais j'espérais avoir un deuxième avis. Avez-vous un pointeur pour les arguments d'en-tête?
Karl Guertin
Voici le seul que j'ai mis en signet (voir la discussion dans les commentaires): barelyenough.org/blog/2008/05/versioning-rest-web-services Un autre site tournait autour de l'utilisation par Ruby de .json, .xml, .wthing pour déterminer le type de contenu d'une demande. Quelques exemples: * langue - le mettre dans l'URL signifie que l'envoi du lien vers un autre pays le rendrait dans la mauvaise langue. * pagination - Le mettre dans l'en-tête signifie que vous ne pouvez pas lier les gens à ce que vous voyez
Richard Levasseur
* content-type: une combinaison de problèmes de langage et de pagination - si c'est dans l'URL, que se passe-t-il si le client ne prend pas en charge ce type de contenu (par exemple, une extension .ajax et une extension .html)? Inversement, sans ce type de contenu dans l'URL, vous ne pouvez pas vous assurer que la même représentation est donnée. "nouveau site ajax! example.com/cool.ajax" vs "article sympa ici: example.com/article.ajax#id=123".
Richard Levasseur le
2
IMO, que ce soit dans l'URL ou non dépend de ce que c'est. Ma règle générale est que si elle identifie une ressource concrète (que ce soit une ressource dans un état spécifique, une sélection de ressources ou un résultat discret), elle va dans l'URL. Les requêtes de recherche, la pagination et les transactions reposantes en sont de bons exemples. Si c'est quelque chose qui est nécessaire pour transformer la représentation abstraite en une représentation concrète, cela va dans l'en-tête. Les informations d'authentification et le type de contenu en sont de bons exemples.
Richard Levasseur le
Je pense à la chaîne de requête dans une URL comme des options pour interroger la ressource spécifiée.
wprl
3

Vous pouvez envisager d'utiliser un modèle comme le Atom Feed Protocol car il a un modèle HTTP sain de collections et comment les manipuler (où insane signifie WebDAV).

Il existe le protocole de publication Atom qui définit le modèle de collection et les opérations REST, ainsi que la RFC 5005 - Feed Paging and Archiving pour parcourir de grandes collections.

Le passage du contenu Atom XML au contenu JSON ne devrait pas affecter l'idée.

Dajobe
la source
3

Je pense que le vrai problème ici est qu'il n'y a rien dans la spécification qui nous indique comment faire des redirections automatiques face à 413 - Requested Entity Too Large.

J'étais aux prises avec ce même problème récemment et j'ai cherché l'inspiration dans le livre RESTful Web Services . Personnellement, je ne pense pas que 206 soit approprié en raison de l'exigence d'en-tête. Mes pensées m'ont également conduit à 300, mais je pensais que c'était plus pour différents types de mime, alors j'ai regardé ce que Richardson et Ruby avaient à dire sur le sujet dans l'annexe B, page 377. Ils suggèrent que le serveur choisisse simplement le serveur préféré. représentation et renvoyez-le avec un 200, en ignorant fondamentalement la notion que ce devrait être un 300.

Cela concorde également avec la notion de liens vers les prochaines ressources que nous avons d'atome. La solution que j'ai implémentée était d'ajouter les clés «suivant» et «précédent» à la carte json que je renvoyais et en finir avec elle.

Plus tard, j'ai commencé à penser que la chose à faire est peut-être d'envoyer un 307 - Redirection temporaire vers un lien qui serait quelque chose comme / db / questions / 1,25 - qui laisse l'URI d'origine comme nom canonique de la ressource, mais cela vous amène à une ressource subordonnée correctement nommée. C'est un comportement que j'aimerais voir sur un 413, mais le 307 me semble un bon compromis. Cependant, je n'ai pas encore essayé cela dans le code. Ce qui serait encore mieux, c'est que la redirection soit redirigée vers une URL contenant les identifiants réels des questions les plus récemment posées. Par exemple, si chaque question a un identifiant entier, et qu'il y a 100 questions dans le système et que vous voulez afficher les dix plus récentes, les demandes à / db / questions doivent être 307 à / db / questions / 100,91

C'est une très bonne question, merci de l'avoir posée. Vous m'avez confirmé que je ne suis pas fou d'avoir passé des jours à y penser.

stinkymatt
la source
303 serait mieux à cet égard que 307. 307 implique que l'URL d'origine commencera bientôt à répondre comme le client l'attend.
Nicholas Shanks
La RFC 7231 fait référence au code d'état HTTP 413 comme Payload Too Large et associe ce code à la taille de la demande et non à la taille de la réponse potentielle.
beawolf
1

Vous pouvez détecter l'en- Rangetête et imiter Dojo s'il est présent, et imiter Atom si ce n'est pas le cas. Il me semble que cela divise parfaitement les cas d'utilisation. Si vous répondez à une requête REST de votre application, vous vous attendez à ce qu'elle soit mise en forme avec un en- Rangetête. Si vous répondez à un navigateur occasionnel, si vous renvoyez des liens de pagination, l'outil vous permettra d'explorer facilement la collection.

Greg
la source
1

L'un des gros problèmes des en-têtes de plage est que de nombreux proxys d'entreprise les filtrent. Je conseillerais d'utiliser un paramètre de requête à la place.

user64141
la source
0

Il me semble que la meilleure façon de procéder consiste à inclure la plage en tant que paramètres de requête. par exemple, GET / db / questions /? date> mindate & date <maxdate . Lors d'un GET vers / db / questions / sans paramètres de requête, renvoyez 303 avec Location: / db / questions /? Query-parameters-to-retrieve-the-default-page . Ensuite, fournissez une URL différente par laquelle quiconque consomme votre API pour obtenir des statistiques sur la collection (par exemple, quels paramètres de requête utiliser s'il / elle veut la collection entière);

Dathan
la source
0

Bien qu'il soit possible d'utiliser l'en-tête Range à cette fin, je ne pense pas que c'était l'intention. Il semble avoir été conçu pour gérer les connexions irrégulières ainsi que pour limiter les données (afin que le client puisse demander une partie de la requête si quelque chose manquait ou si la taille était trop grande pour être traitée). Vous piratez la pagination dans quelque chose qui est probablement utilisé à d'autres fins au niveau de la couche de communication. La "bonne" façon de gérer la pagination est d'utiliser les types que vous renvoyez. Plutôt que de renvoyer l'objet de questions, vous devriez renvoyer un nouveau type à la place.

Donc, si les questions sont comme ça:

<questions> <question index=1></question> <question index=2></question> ... </questions>

Le nouveau type pourrait être quelque chose comme ceci:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

Bien sûr, vous contrôlez vos types de médias, vous pouvez donc faire de vos "pages" un format qui répond à vos besoins. Si vous créez quelque chose de générique, vous pouvez avoir un seul analyseur sur le client pour gérer la pagination de la même manière pour tous les types. Je pense que c'est plus dans l'esprit de la spécification HTTP, plutôt que de truquer le paramètre Range pour autre chose.

Jeremyh
la source