web-dev-qa-db-fra.com

Paging dans une collection de repos

Je souhaite exposer une interface directe REST à des collections de documents JSON (think CouchDB ou Persevere ). Le problème que je rencontre est de savoir comment gérer l'opération GET sur la racine de la collection si la collection est volumineuse.

Par exemple, supposons que j'expose la table Questions de StackOverflow dans laquelle chaque ligne est exposée en tant que document (pas qu'il existe nécessairement une telle table, mais juste un exemple concret d'une importante collection de 'documents'). La collection serait mise à disposition au /db/questions avec l’appel CRUD habituel GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questions en cours de lecture. La méthode standard pour obtenir la totalité de la collection consiste à GET /db/questions, mais si cela vide naïvement chaque ligne en tant qu’objet JSON, vous aurez un téléchargement assez volumineux 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 astucieuse conforme à la norme RFC2616 consistant à utiliser l'en-tête Range avec une unité de plage personnalisée items. Le résultat est un 206 Partial Content qui renvoie uniquement la plage demandée. L’avantage de cette approche par rapport à un paramètre de requête est qu’il laisse la chaîne de requête pour ... requêtes (par exemple, GET /db/questions/?score>200 ou une telle méthode, et oui, elle serait codée %3E).

Cette approche couvre complètement le comportement que je veux. Le problème est que RFC 2616 précise que sur une réponse 206 (mon accentuation):

La demande DOIT avoir inclus un champ d'en-tête Range ( section 14.35 ) indiquant la plage souhaitée, et PEUT avoir inclus une gamme If champ d'en-tête ( 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 je souhaiterais que la réponse 206 soit la réponse par défaut à la gestion de clients naïfs/de personnes aléatoires.

J'ai examiné la RFC en détail à la recherche d'une solution, mais je suis mécontent de mes solutions et je suis intéressé par la résolution du problème. 

Idées que j'ai eues:

  • Retournez 200 avec un en-tête Content-Range! _ Je ne pense pas que cela soit faux, mais je préférerais un indicateur plus évident que la réponse est seulement un contenu partiel.
  • Return 400 Range Required - Il n'y a pas de code de réponse spécial 400 pour les en-têtes requis. L'erreur par défaut doit donc être utilisée et lue à la main. Cela rend également l'exploration via un navigateur Web (ou un autre client tel que 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, ce qui coupe l'espace de noms de la requête.
  • _ {Il suffit de renvoyer 206! - Je pense que la plupart des clients ne paniqueraient pas, mais je préférerais ne pas aller contre un MUST dans le RFC.
  • Étendre la spécification! Return 266 Partial Content - Se comporte exactement comme 206 mais répond à une demande qui NE DOIT PAS contenir l'en-tête Range. Je pense que 266 est suffisamment élevé pour ne pas me heurter à des problèmes de collision. Cela me semble logique, mais je ne vois pas très bien 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 de manière factuelle afin que moi-même ou quelqu'un d'autre ne réinventions pas la roue.

Quel est le meilleur moyen d'exposer une collection complète via HTTP lorsque la collection est volumineuse?

121
Karl Guertin

Mon sentiment est que les extensions de la 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 206 et 206 ne doit être envoyé que si le client le demande.

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 renvoyée avec un statut 200 et éventuellement des liens de pagination). Voir RFC 4287 et RFC 5005 .

21
Julian Reschke

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 REST appelle 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 à traiter une erreur 413 REQUISE ENTITY TOO LARGE lorsque la collection demandée est trop volumineuse pour pouvoir être extraite en un seul aller-retour.

Le serveur envoie une réponse 206 PARTIAL CONTENT, avec l'en-tête Content-Range spécifiant la partie de la ressource qui 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}} semblable à Facebook, et je considère que l'ETag d'une collection est celui de la dernière ressource modifiée qu'elle contient.

Pour demander une partie spécifique d'une collection, le client DOIT utiliser l'en-tête "Range" et renseigner l'en-tête "If-Match" avec l'ETag de la collection obtenue à partir de demandes précédemment exécuté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 ont peut-être é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 ou les deux pour leurs algorithmes de mise en cache.

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

32
Mohamed

S'il y a plus d'une page de réponses et que vous ne voulez pas offrir la collection complète en même temps, cela signifie-t-il qu'il y a plusieurs choix?

Sur une demande adressée à /db/questions, renvoyez 300 Multiple Choices avec les en-têtes Linkqui 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-tête Linkpour chaque page de résultats (une chaîne vide indique l'URL actuelle, et l'URL est la même pour chaque page, accessible uniquement avec des plages différentes), et la relation est définie comme une valeur personnalisée par la prochaine spécification Linkname__ . Cette relation expliquerait votre 266 personnalisé ou 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 "plage", je pense que votre propre code de retour 2xx, tel que vous l'avez décrit, serait le meilleur comportement à adopter ici. Vous êtes censé le faire pour vos applications et autres ["HTTP les codes de statut sont extensibles. "], et vous avez de bonnes raisons.)

300 Multiple Choices indique que vous DEVEZ également fournir un corps avec un moyen de sélection pour l'agent utilisateur. Si votre client comprend, il doit utiliser les en-têtes Linkname__. Si c'est un utilisateur qui navigue manuellement, peut-être une page HTML avec des liens vers une ressource racine spéciale "paginée" pouvant gérer le rendu de cette page particulière en fonction de l'URL? /humanpage/1/db/questions ou quelque chose d'aussi hideux?


Les commentaires sur le message de Richard Levasseur me font penser à une option supplémentaire: l'en-tête Accept(section 14.1). À l'époque de la publication de la spécification oEmbed, je me suis demandé pourquoi cela n'avait pas été fait entièrement avec HTTP et j'ai écrit une alternative pour les utiliser.

Conservez les en-têtes 300 Multiple Choices, Linket la page HTML pour un nom HTTP naïf GET____ naïf, mais plutôt que des plages d'utilisation, définissez l'utilisation de l'en-tête Acceptname__. Votre requête HTTP ultérieure pourrait ressembler à ceci:

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

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

5
Vitorio

Vous pouvez toujours renvoyer Accept-Ranges et Content-Ranges avec un code de réponse 200. Ces deux en-têtes de réponse vous fournissent suffisamment d'informations pour infer les mêmes informations qu'un code de réponse 206 fournit explicitement.

J'utiliserais Range pour la pagination et le renverrais simplement un 200 pour un GET simple.

Cela donne 100% de repos et ne rend pas la navigation plus difficile.

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

5
John Gietzen

Je pense que le vrai problème ici est que rien dans la spécification ne nous dit comment faire des redirections automatiques face à 413 - Entité demandée trop grande.

Je me débattais avec le même problème récemment et je cherchais de 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 amené à 300, mais je pensais que c'était plus pour différents types de mimes, alors j'ai regardé ce que Richardson et Ruby avaient à dire à ce sujet à l'Annexe B, page 377. Ils suggèrent que le serveur choisisse simplement le fichier préféré. représentation et le renvoyer avec un 200, en ignorant essentiellement la notion qu'il devrait être un 300.

Cela concorde également avec la notion de liens vers les ressources suivantes que nous avons d’atome. La solution que j’ai mise en œuvre consistait à ajouter les clés "next" et "previous" à la carte json que je renvoyais et d’en finir.

Plus tard, j'ai commencé à penser que la chose à faire était peut-être d'envoyer 307 - Redirections temporaires vers un lien qui ressemblerait à quelque chose du genre/db/questions/1,25 - qui laisse l'URI d'origine en tant que nom de ressource canonique, mais cela vous amène à une ressource subordonnée nommée de manière appropriée. C'est un comportement que j'aimerais voir sur un 413, mais le 307 semble un bon compromis. Je n'ai pas encore réellement essayé cela dans le code. Ce qui serait encore mieux, c’est que la redirection redirige vers une URL contenant les identifiants réels des dernières questions posées. Par exemple, si chaque question a un ID entier et qu'il y a 100 questions dans le système et que vous souhaitez afficher les dix plus récentes, les demandes adressées à/db/questions doivent être traitées dans/db/questions/100,91.

C'est une très bonne question, merci de le poser. Vous m'avez confirmé que je ne suis pas cinglé pour avoir passé des jours à y penser.

3
stinkymatt

Modifier:

Après y avoir réfléchi un peu plus, je suis enclin à convenir que les en-têtes Range ne conviennent pas à la pagination. La logique étant, l'en-tête Range est destiné à la réponse du serveur, pas aux applications. Si vous avez servi 100 mégaoctets de résultats, mais que le serveur (ou le client) ne peut traiter qu'un mégaoctet à la fois, eh bien, c'est à quoi 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). Elle mérite donc d'être représentée dans l'URL.

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


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

Vous avez essentiellement des objectifs contradictoires: permettre aux utilisateurs d’utiliser leur navigateur pour explorer (ce qui ne permet pas facilement des en-têtes personnalisés) ou les forcer à utiliser un client spécial pouvant 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 simple navigateur, envoyez une petite application ajax qui restitue la page et définit les en-têtes nécessaires.

Bien sûr, il y a aussi le débat sur le fait de savoir si l'URL doit contenir tout l'état nécessaire pour ce genre de chose. La spécification de la plage à l'aide d'en-têtes peut être considérée comme "non reposante" par certains.

Soit dit en passant, il serait agréable que les serveurs répondent avec un en-tête "Spécification canable: en-tête1, en-tête2" et que les navigateurs Web présentent une interface utilisateur permettant aux utilisateurs de saisir des valeurs s'ils le souhaitent.

3

Vous pouvez envisager d'utiliser un modèle similaire au protocole de flux Atom, car il possède un modèle HTTP sain de collections et explique comment les manipuler (où alias WebDAV). 

Il existe le Atom Publishing Protocol qui définit le modèle de collection et les opérations REST. Vous pouvez également utiliser RFC 5005 - Pagination et archivage de flux pour parcourir de grandes collections.

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

3
dajobe

Avec la publication de rfc723x , unités de plage non enregistrées vont à l’encontre d’une recommandation explicite dans la spécification. Considérez rfc7233 (dépréciant rfc2616):

" Les nouvelles unités de gamme doivent être enregistrées auprès de IANA " (avec une référence à un HTTP Range Unit Registry ).

1
Sam

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

1
Greg

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

1
user64141

Il me semble que la meilleure façon de faire est d’inclure une plage comme paramètre de requête. Par exemple, GET/db/questions /? date> état & date <date max. . Sur un objet GET dans/db/questions/sans paramètre de requête, retournez 303 avec Emplacement:/db/questions /? Query-parameters-to-extraire-la-page-par-défaut . Indiquez ensuite une autre URL, en fonction de la consommation de votre API, pour obtenir des statistiques sur la collection (par exemple, quels paramètres de requête utiliser si il/elle souhaite disposer de la totalité de la collection);

0
Dathan

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 floconneuses et limiter les données (afin que le client puisse demander une partie de la demande si quelque chose manquait ou si la taille était trop grande pour être traitée). Vous piratez la pagination en un objet susceptible d’être utilisé à d’autres fins au niveau de la couche communication ... La méthode "appropriée" pour gérer la pagination consiste à utiliser les types que vous renvoyez. Plutôt que de renvoyer l'objet de questions, vous devriez plutôt renvoyer un nouveau type. 

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 supports afin que vous puissiez faire de vos "pages" un format adapté à vos besoins. Si vous faites 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 cela est plus dans l'esprit de la spécification HTTP, plutôt que de modifier le paramètre Range pour autre chose.

0
jeremyh