it-swarm.com.de

Paging in einer Rest-Sammlung

Ich bin daran interessiert, eine direkte REST - Schnittstelle für Sammlungen von JSON-Dokumenten bereitzustellen (think CouchDB oder Persevere ). Das Problem, mit dem ich konfrontiert bin, ist die Behandlung der GET-Operation im Auflistungsstamm, wenn die Auflistung groß ist.

Als Beispiel gebe ich vor, ich zeige die Questions-Tabelle von StackOverflow, in der jede Zeile als Dokument verfügbar gemacht wird (nicht, dass es notwendigerweise eine solche Tabelle gibt, nur ein konkretes Beispiel für eine ansehnliche Sammlung von "Dokumenten"). Die Sammlung würde unter /db/questions zur Verfügung stehen, wobei die übliche CRUD-API GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questions im Spiel ist. Die Standardmethode, um die gesamte Sammlung abzurufen, ist GET /db/questions. Wenn jedoch jede Zeile auf naive Weise als JSON-Objekt abgelegt wird, erhalten Sie einen recht umfangreichen Download und eine Menge Arbeit seitens des Servers. 

Die Lösung ist natürlich Paging. Dojo hat dieses Problem in seinem JsonRestStore über eine clevere RFC2616-kompatible Erweiterung der Verwendung des Range-Headers mit einer benutzerdefinierten Bereichseinheit items gelöst. Das Ergebnis ist ein 206 Partial Content, der nur den angeforderten Bereich zurückgibt. Der Vorteil dieses Ansatzes gegenüber einem Abfrageparameter besteht darin, dass die Abfragezeichenfolge für ... Abfragen (z. B. GET /db/questions/?score>200 oder etwas anderes und ja, die %3E codiert werden würden) belassen wird.

Dieser Ansatz deckt das von mir gewünschte Verhalten vollständig ab. Das Problem ist, dass RFC 2616 das auf einer 206-Antwort (Hervorhebungsmine) angibt:

Die Anfrage request MUSS ein Range-Header-Feld ( Abschnitt 14.35 ) .__ enthalten. zeigt den gewünschten Bereich an und kann einen If-Bereich enthalten Header-Feld ( Abschnitt 14.27 ), um die Anforderung bedingt zu machen.

Dies ist im Zusammenhang mit der Standardverwendung des Headers sinnvoll, stellt jedoch ein Problem dar, weil ich möchte, dass die Antwort 206 die Standardeinstellung ist, mit der naive Clients/zufällige Personen untersucht werden.

Ich habe den RFC im Detail durchgesehen und nach einer Lösung gesucht, war aber mit meinen Lösungen unzufrieden und interessiere mich dafür, dass SOs das Problem annehmen. 

Ideen, die ich hatte:

  • Rückgabe von 200 mit einem Content-Range-Header! - Ich denke nicht, dass dies falsch ist, aber ich würde es vorziehen, wenn ein offensichtlicherer Hinweis darauf wäre, dass die Antwort nur Teilinhalt ist.
  • Return_400 Range Required - Es gibt keinen speziellen 400-Antwortcode für die erforderlichen Header, daher muss der Standardfehler verwendet und von Hand gelesen werden. Dies erschwert auch die Suche über einen Webbrowser (oder einen anderen Client wie Resty).
  • Verwenden Sie einen Abfrageparameter - Der Standardansatz, aber ich hoffe, dass Abfragen a la Persevere zugelassen werden können, wodurch der Abfrage-Namespace eingeschränkt wird.
  • Nur 206 zurückgeben! - Ich denke, die meisten Kunden würden nicht ausflippen, aber ich möchte lieber nicht gegen ein MUST im RFC gehen
  • Erweitern Sie die Spezifikation! Return 266 Partial Content - Verhält sich genau wie 206, reagiert jedoch auf eine Anforderung, die NICHT den Header Range enthalten darf. Ich denke, 266 ist hoch genug, dass ich nicht auf Kollisionsprobleme stoßen sollte, und es ist für mich sinnvoll, aber ich weiß nicht, ob dies als Tabu gilt oder nicht.

Ich denke, das ist ein ziemlich häufiges Problem und ich möchte, dass dies auf eine Art de facto Art und Weise geschieht, so dass ich oder jemand anderes das Rad nicht neu erfinden.

Was ist der beste Weg, um eine vollständige Sammlung über HTTP verfügbar zu machen, wenn die Sammlung groß ist?

121
Karl Guertin

Mein Bauchgefühl ist, dass die Erweiterungen der HTTP-Bereiche nicht für Ihren Anwendungsfall ausgelegt sind, und Sie sollten es daher nicht versuchen. Eine Teilantwort impliziert, dass 206 und 206 nur gesendet werden muss, wenn der Client danach gefragt hat.

Möglicherweise möchten Sie einen anderen Ansatz in Betracht ziehen, z. B. den in Atom verwendeten Ansatz (bei dem die Darstellung nach Entwurf partiell sein kann und der Status 200 und möglicherweise Paging-Links zurückgegeben wird). Siehe RFC 4287 und RFC 5005 .

21
Julian Reschke

Ich stimme mit einigen von euch nicht wirklich überein. Ich arbeite seit Wochen an diesen Funktionen für meinen REST - Dienst. Was ich am Ende gemacht habe, ist sehr einfach. Meine Lösung macht nur einen Sinn für das, was REST als Sammlung bezeichnet wird.

Der Client MUSS einen "Range" -Header einschließen, um anzuzeigen, welcher Teil der Sammlung er benötigt, oder er muss anderweitig bereit sein, einen 413 REQUESTED ENTITY TOO LARGE-Fehler abzuwickeln, wenn die angeforderte Sammlung zu groß ist, um in einem einzigen Roundtrip abgerufen zu werden.

Der Server sendet eine Antwort mit dem Inhalt des Bereichs, in der angegeben ist, welcher Teil der Ressource gesendet wurde, und ein ETag-Header, um die aktuelle Version der Sammlung zu identifizieren. Normalerweise verwende ich ein ETag, das wie Facebook aussieht, {last_modification_timestamp} - {resource_id}, und ich denke, dass das ETag einer Sammlung das der zuletzt geänderten Ressource ist, die es enthält.

Um einen bestimmten Teil einer Sammlung anzufordern, MUSS der Client den Header "Range" verwenden und den "If-Match" -Header mit dem ETag der Sammlung füllen, die aus zuvor durchgeführten Anforderungen abgerufen wurde, um andere Teile derselben Sammlung abzurufen. Der Server kann daher überprüfen, dass sich die Sammlung nicht geändert hat, bevor der angeforderte Teil gesendet wird. Wenn eine neuere Version vorhanden ist, wird eine Antwort 412 PRECONDITION FAILED zurückgegeben, um den Client einzuladen, die Sammlung von Grund auf abzurufen. Dies ist notwendig, da dies möglicherweise bedeutet, dass einige Ressourcen vor oder nach dem aktuell angeforderten Teil hinzugefügt oder entfernt wurden.

Ich verwende ETag/If-Match in Kombination mit Last-Modified/If-Unmodified-Since, um den Cache zu optimieren. Browser und Proxys können sich für einen Zwischenspeicherungsalgorithmus auf einen oder beide verlassen.

Ich denke, eine URL sollte sauber sein, es sei denn, sie enthält eine Such-/Filterabfrage. Wenn Sie darüber nachdenken, ist eine Suche nichts anderes als eine Teilansicht einer Sammlung. Anstelle der Autos/Suche? Q = BMW URLs, sollten mehr Autos angezeigt werden? Hersteller = BMW.

32
Mohamed

Wenn es mehrere Antwortseiten gibt und Sie nicht die gesamte Sammlung auf einmal anbieten möchten, bedeutet das, dass es mehrere Auswahlmöglichkeiten gibt?

Bei einer Anforderung an /db/questions geben Sie 300 Multiple Choices mit Link-Kopfzeilen zurück, die angeben, wie auf jede Seite zugegriffen werden soll, sowie ein JSON-Objekt oder eine HTML-Seite mit einer Liste von URLs.

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

Sie hätten einen Link-Header für jede Ergebnisseite (ein leerer String steht für die aktuelle URL und die URL ist für jede Seite gleich, auf die gerade mit unterschiedlichen Bereichen zugegriffen wird). Die Beziehung wird als eine benutzerdefinierte pro definiert die kommende Link spec . Diese Beziehung würde Ihren benutzerdefinierten 266 oder Ihre Verletzung von 206 erklären. Diese Header sind Ihre maschinenlesbare Version, da für alle Ihre Beispiele ohnehin ein verständnisvoller Client erforderlich ist.

(Wenn Sie bei der "Range" -Route bleiben, glaube ich, würde Ihr eigener 2xx-Rückkehrcode, wie Sie ihn beschrieben haben, hier das beste Verhalten sein. Es wird erwartet, dass Sie dies für Ihre Anwendungen tun, und solche HTTP-Statuscodes sind erweiterbar . "], und Sie haben gute Gründe.)

300 Multiple Choices sagt, Sie MÜSSEN auch einen Körper mit einer Möglichkeit für den Benutzeragenten zur Auswahl bereitstellen. Wenn Ihr Client Verständnis hat, sollte er die Header Link verwenden. Wenn es sich um einen Benutzer handelt, der manuell browst, vielleicht eine HTML-Seite mit Links zu einer speziellen "paged" -Rootressource, die das Rendern dieser bestimmten Seite basierend auf der URL handhaben kann? /humanpage/1/db/questions oder so was Hässliches?


Die Kommentare zu Richard Levasseurs Beitrag erinnern mich an eine zusätzliche Option: den Accept-Header (Abschnitt 14.1). Damals, als die Spezifikation von oEmbed herauskam, fragte ich mich, warum es nicht vollständig mit HTTP gemacht wurde, und schrieb eine Alternative auf.

Behalten Sie die 300 Multiple Choices, die Link-Header und die HTML-Seite für ein anfängliches naives HTTP GET bei. Lassen Sie jedoch keine Bereiche verwenden, sondern definieren Sie die Verwendung des Accept-Headers. Ihre nachfolgende HTTP-Anfrage könnte folgendermaßen aussehen:

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

Mit dem Accept-Header können Sie einen akzeptablen Inhaltstyp (Ihre JSON-Rückgabe) sowie erweiterbare Parameter für diesen Typ (Ihre Seitennummer) definieren. Riffing auf meine Notizen aus meinem oEmbed-Beitrag (kann hier nicht verlinkt werden, ich liste es in meinem Profil auf), könnten Sie sehr explizit sein und eine Spezifikations-/Beziehungsversion angeben, falls Sie den Parameter page neu definieren müssen bedeutet in der Zukunft.

5
Vitorio

Sie können Accept-Ranges und Content-Ranges noch mit einem 200-Antwortcode zurückgeben. Diese beiden Antwortheader geben Ihnen genügend Informationen, um infer dieselben Informationen zu erhalten, die ein 206-Antwortcode explizit enthält.

Ich würde Range für die Paginierung verwenden und habe einfach einen 200 für eine einfache GET zurückgeben.

Dies fühlt sich zu 100% RESTful an und macht das Surfen nicht schwieriger.

Edit: Ich habe einen Blogbeitrag darüber geschrieben: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

5
John Gietzen

Ich denke, das eigentliche Problem hier ist, dass die Spezifikation nichts enthält, was uns sagt, wie automatische Weiterleitungen bei 413 - Requested Entity Too Large durchgeführt werden müssen.

Ich hatte vor kurzem mit diesem Problem zu kämpfen und suchte nach Inspiration im RESTful Web Services Buch. Ich persönlich denke nicht, dass 206 aufgrund der Header-Anforderung angemessen ist. Meine Gedanken führten mich auch zu 300, aber ich dachte, das war mehr für verschiedene Mime-Typen, also schaute ich nach, was Richardson und Ruby in Anhang B, Seite 377 zu diesem Thema zu sagen hatten Darstellung und senden Sie es mit einer 200 zurück, im Grunde ignoriert die Vorstellung, dass es eine 300 sein sollte.

Das stößt auch auf die Vorstellung von Verbindungen zu den nächsten Ressourcen, die wir von Atom haben. Die Lösung, die ich implementierte, bestand darin, der Json-Map, die ich zurücksandte, "next" und "previous" -Tasten hinzuzufügen und damit fertig zu werden.

Später habe ich darüber nachgedacht, dass Sie vielleicht eine 307 - Temporäre Weiterleitung an einen Link senden möchten, der etwa wie/db/questions/1,25 aussieht - und dabei den ursprünglichen URI als kanonischen Ressourcennamen belässt eine entsprechend benannte untergeordnete Ressource. Dies ist ein Verhalten, das ich gerne von einer 413 sehen würde, aber 307 scheint ein guter Kompromiss zu sein. Habe das allerdings noch nicht im Code versucht. Noch besser wäre es, wenn die Weiterleitung an eine URL umgeleitet wird, die die tatsächlichen IDs der zuletzt gestellten Fragen enthält. Wenn zum Beispiel jede Frage eine ganzzahlige ID hat und 100 Fragen im System vorhanden sind und Sie die letzten zehn anzeigen möchten, sollten Anfragen an/db/Fragen 307 Punkte an/db/questions/100,91 lauten

Dies ist eine sehr gute Frage, danke, dass Sie sie gefragt haben. Sie haben für mich bestätigt, dass ich nicht verrückt bin, weil ich tagelang darüber nachgedacht habe.

3
stinkymatt

Bearbeiten:

Nachdem ich noch etwas darüber nachgedacht habe, stimme ich zu, dass Range-Header nicht für die Paginierung geeignet sind. Die Logik ist, der Range-Header ist für die Antwort des Servers bestimmt, nicht für die Anwendungen. Wenn Sie 100 Megabyte Ergebnisse geliefert haben, der Server (oder Client) jedoch nur jeweils 1 Megabyte verarbeiten kann, ist dies der Range-Header.

Ich bin auch der Meinung, dass eine Teilmenge von Ressourcen eine eigene Ressource ist (vergleichbar mit relationaler Algebra.), Daher verdient sie die Darstellung in der URL.

Im Grunde empfehle ich meine ursprüngliche Antwort (unten) über die Verwendung eines Headers.


Ich denke, Sie haben Ihre eigene Frage mehr oder weniger beantwortet - geben Sie 200 oder 206 mit Inhaltsbereich zurück und verwenden Sie optional einen Abfrageparameter. Ich würde den Benutzeragenten und den Inhaltstyp ausfindig machen und, je nachdem, nach einem Abfrageparameter suchen. Ansonsten benötigen Sie die Bereichsüberschriften.

Sie haben im Wesentlichen widersprüchliche Ziele. Lassen Sie die Benutzer ihren Browser zum Durchsuchen verwenden (was benutzerdefinierte Kopfzeilen nicht einfach zulässt), oder zwingen Sie die Benutzer, einen speziellen Client zu verwenden, der Kopfzeilen festlegen kann (wodurch sie nicht erkundet werden können).

Sie können ihnen je nach Anforderung einfach den speziellen Client zur Verfügung stellen. Wenn es wie ein einfacher Browser aussieht, senden Sie eine kleine Ajax-App herunter, die die Seite rendert und die erforderlichen Header festlegt.

Natürlich gibt es auch die Debatte darüber, ob die URL alle für diese Art von Status erforderlichen Status enthalten soll. Die Angabe des Bereichs mithilfe von Kopfzeilen kann von einigen als "unruhig" betrachtet werden.

Abgesehen davon wäre es schön, wenn Server mit einem "Can-Specify: Header1, header2" -Header antworten könnten und Webbrowser eine Benutzeroberfläche darstellen würden, mit der die Benutzer bei Bedarf Werte eingeben können.

3

Sie könnten in Betracht ziehen, ein Modell wie das Atom-Feed-Protokoll zu verwenden, da es ein vernünftiges HTTP-Modell mit Sammlungen und deren Manipulationen gibt (wobei Wahnsinnig WebDAV bedeutet). 

Es gibt das Atom Publishing Protocol , das das Sammlungsmodell und REST -Operationen definiert, und Sie können RFC 5005 - Feed Paging und Archivierung verwenden, um durch große Sammlungen zu blättern.

Der Wechsel von Atom XML zu JSON-Inhalten sollte keinen Einfluss auf die Idee haben.

3
dajobe

Mit der Veröffentlichung von rfc723x widersprechen unregistrierte Bereichseinheiten einer expliziten Empfehlung in der Spezifikation. Betrachten Sie rfc7233 (veraltet rfc2616):

" Neue Range Units sollten bei IANA registriert werden (zusammen mit einem Verweis auf eine HTTP Range Unit Registry ).

1
Sam

Sie können den Range-Header erkennen und Dojo nachahmen, falls vorhanden, und Atom nachahmen, wenn dies nicht der Fall ist. Es scheint mir, dass dies die Anwendungsfälle sauber teilt. Wenn Sie auf eine REST - Abfrage Ihrer Anwendung antworten, erwarten Sie, dass diese mit einem Range-Header formatiert wird. Wenn Sie auf einen zufälligen Browser reagieren, können Sie bei der Rückgabe von Paging-Links das Tool auf einfache Weise die Sammlung anzeigen lassen.

1
Greg

Eines der größten Probleme bei Range-Headern ist, dass viele Unternehmens-Proxies sie herausfiltern. Ich würde empfehlen, stattdessen einen Abfrageparameter zu verwenden. 

1
user64141

Es scheint mir, dass der beste Weg, dies zu tun, darin besteht, den Bereich als Abfrageparameter aufzunehmen. z. B. GET/db/questions /? date> mindate & date <maxdate . Bei einem GET für die Parameter/db/questions/ohne Abfrageparameter 303 mit Location:/db/questions /? Abfrageparameter-zum-Abrufen der Standardseite zurückgeben. Geben Sie dann eine andere URL an, unter der die API Ihre API verwendet, um Statistiken über die Sammlung zu erhalten (z. B. welche Abfrageparameter verwendet werden sollen, wenn die gesamte Sammlung gewünscht wird).

0
Dathan

Es ist zwar möglich, den Range-Header für diesen Zweck zu verwenden, aber ich glaube nicht, dass dies die Absicht war. Es scheint für die Handhabung flockiger Verbindungen und die Begrenzung der Daten konzipiert worden zu sein (der Client kann einen Teil der Anforderung anfordern, wenn etwas fehlt oder die Größe zu groß ist, um verarbeitet zu werden). Sie hacken die Paginierung in etwas ein, das wahrscheinlich für andere Zwecke auf der Kommunikationsschicht verwendet wird .. Die "richtige" Methode zur Handhabung der Paginierung besteht in den Typen, die Sie zurückgeben. Anstatt ein Fragenobjekt zurückzugeben, sollten Sie stattdessen einen neuen Typ zurückgeben. 

Also wenn Fragen so sind:

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

Der neue Typ könnte so aussehen:

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

Natürlich kontrollieren Sie Ihre Medientypen, sodass Sie Ihre "Seiten" in ein Format bringen können, das Ihren Anforderungen entspricht. Wenn Sie etwas generisch machen, können Sie einen einzelnen Parser auf dem Client verwenden, um das Auslagern für alle Typen gleich zu behandeln. Ich denke, das ist mehr im Sinne der HTTP-Spezifikation, als den Range-Parameter für etwas anderes zu verfälschen.

0
jeremyh