Was ist die beste RESTful-Methode, um die Gesamtzahl der Elemente in einem Objekt zurückzugeben?

138

Ich entwickle einen REST-API-Service für eine große Website für soziale Netzwerke, an der ich beteiligt bin. Bisher funktioniert er hervorragend. Ich kann ausgeben GET, POST, PUTund DELETEAnfragen an Objekt - URLs und beeinflussen meine Daten. Diese Daten werden jedoch ausgelagert (auf jeweils 30 Ergebnisse begrenzt).

Was wäre jedoch der beste REST-Weg, um die Gesamtzahl der Mitglieder über meine API zu ermitteln?

Derzeit stelle ich Anforderungen an eine URL-Struktur wie die folgende:

  • / api / members - Gibt eine Liste der Mitglieder zurück (jeweils 30 wie oben erwähnt).
  • / api / member / 1 - Beeinflusst ein einzelnes Mitglied, abhängig von der verwendeten Anforderungsmethode

Meine Frage ist: Wie würde ich dann eine ähnliche URL-Struktur verwenden, um die Gesamtzahl der Mitglieder in meiner Bewerbung zu erhalten? Offensichtlich idwäre es unwirksam , nur das Feld anzufordern (ähnlich wie bei der Graph-API von Facebook) und die Ergebnisse zu zählen, da nur ein Teil von 30 Ergebnissen zurückgegeben würde.

Martin Bean
quelle

Antworten:

84

Während die Antwort an / API / users ausgelagert ist und nur 30 Datensätze zurückgibt, hindert Sie nichts daran, auch die Gesamtzahl der Datensätze und andere relevante Informationen wie Seitengröße, Seitenzahl / Versatz usw. In die Antwort aufzunehmen .

Die StackOverflow-API ist ein gutes Beispiel für dasselbe Design. Hier ist die Dokumentation für die Users-Methode - https://api.stackexchange.com/docs/users

Franci Penov
quelle
3
+1: Auf jeden Fall das RESTVOLLSTE, was zu tun ist, wenn überhaupt Abruflimits festgelegt werden.
Donal Fellows
2
@bzim Du würdest wissen, dass es eine nächste Seite zum Abrufen gibt, da es einen Link mit rel = "next" gibt.
Darrel Miller
4
@Donal die "nächste" rel ist bei IANA registriert iana.org/assignments/link-relations/link-relations.txt
Darrel Miller
1
@Darrel - ja, dies kann mit jeder Art von "next" -Flag in der Nutzlast durchgeführt werden. Ich bin nur der Meinung, dass die Gesamtzahl der Sammlungselemente in der Antwort für sich genommen wertvoll ist und trotzdem als "nächste" Flagge fungiert.
Franci Penov
5
Das Zurückgeben eines Objekts, das keine Liste von Elementen ist, ist keine ordnungsgemäße Implementierung einer REST-API, aber REST bietet keine Möglichkeit, eine unvollständige Liste der Ergebnisse abzurufen. Um dies zu respektieren, sollten wir Header verwenden, um andere Informationen wie Total, Token für die nächste Seite und Token für die vorherige Seite zu übertragen. Ich habe es nie ausprobiert und brauche Rat von anderen Entwicklern.
Loenix
74

Ich bevorzuge die Verwendung von HTTP-Headern für diese Art von Kontextinformationen.

Für die Gesamtzahl der Elemente verwende ich den X-total-countHeader.
Für Links zur nächsten, vorherigen Seite usw. verwende ich den http- LinkHeader:
http://www.w3.org/wiki/LinkHeader

Github macht es genauso: https://developer.github.com/v3/#pagination

Meiner Meinung nach ist es sauberer, da es auch verwendet werden kann, wenn Sie Inhalte zurückgeben, die keine Hyperlinks unterstützen (dh Binärdateien, Bilder).

Ondrej Bozek
quelle
5
RFC6648 lehnt die Konvention ab, den Namen nicht standardisierter Parameter die Zeichenfolge voranzustellen X-.
JDawg
70

Ich habe in letzter Zeit einige umfangreiche Untersuchungen zu dieser und anderen Fragen im Zusammenhang mit REST-Paging durchgeführt und fand es konstruktiv, einige meiner Ergebnisse hier hinzuzufügen. Ich erweitere die Frage ein wenig, um Gedanken zum Paging sowie zur Zählung einzubeziehen, da sie eng miteinander verbunden sind.

Überschriften

Die Paging-Metadaten sind in Form von Antwortheadern in der Antwort enthalten. Der große Vorteil dieses Ansatzes besteht darin, dass die Antwortnutzlast selbst nur die tatsächliche Datenanforderung ist, nach der gefragt wurde. Erleichterung der Verarbeitung der Antwort für Clients, die nicht an den Paging-Informationen interessiert sind.

Es gibt eine Reihe von (Standard- und benutzerdefinierten) Headern, die in freier Wildbahn verwendet werden, um Paging-bezogene Informationen zurückzugeben, einschließlich der Gesamtzahl.

X-Total-Count

X-Total-Count: 234

Dies wird in einigen APIs verwendet, die ich in freier Wildbahn gefunden habe. Es gibt auch NPM-Pakete zum Hinzufügen von Unterstützung für diesen Header zu z. B. Loopback. In einigen Artikeln wird empfohlen, auch diesen Header festzulegen.

Es wird häufig in Kombination mit dem LinkHeader verwendet. Dies ist eine ziemlich gute Lösung für das Paging, es fehlen jedoch die Informationen zur Gesamtanzahl.

Verknüpfung

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Ich bin der Meinung, dass nach allgemeiner Lektüre zu diesem Thema der allgemeine Konsens darin besteht, den LinkHeader zu verwenden, um Paging-Links für Clients bereitzustellen rel=next, die rel=previoususw. verwenden. Das Problem dabei ist, dass es an Informationen darüber mangelt, wie viele Datensätze insgesamt vorhanden sind warum viele APIs dies mit dem X-Total-CountHeader kombinieren .

Alternativ verwenden einige APIs und z. B. der JsonApi- Standard das LinkFormat, fügen die Informationen jedoch in einem Antwortumschlag anstelle eines Headers hinzu. Dies vereinfacht den Zugriff auf die Metadaten (und schafft einen Ort zum Hinzufügen der Gesamtanzahlinformationen) auf Kosten einer zunehmenden Komplexität des Zugriffs auf die tatsächlichen Daten selbst (durch Hinzufügen eines Umschlags).

Inhaltsbereich

Content-Range: items 0-49/234

Gefördert durch einen Blog-Artikel namens Range Header, wähle ich Sie (für die Paginierung)! . Der Autor spricht sich stark dafür aus, die Überschriften Rangeund Content-Rangefür die Paginierung zu verwenden. Wenn wir den RFC in diesen Headern sorgfältig lesen , stellen wir fest, dass die Erweiterung ihrer Bedeutung über Bytebereiche hinaus vom RFC tatsächlich vorweggenommen wurde und ausdrücklich zulässig ist. Wenn der Bereichskopf im Kontext von itemsanstelle von verwendet wird bytes, können wir sowohl einen bestimmten Bereich von Elementen anfordern als auch angeben, auf welchen Bereich des Gesamtergebnisses sich die Antwortelemente beziehen. Dieser Header bietet auch eine großartige Möglichkeit, die Gesamtzahl anzuzeigen. Und es ist ein echter Standard, der Paging meistens eins zu eins zuordnet. Es wird auch in freier Wildbahn verwendet .

Briefumschlag

Viele APIs, einschließlich der von unserer bevorzugten Q & A-Website, verwenden einen Umschlag , einen Wrapper um die Daten, mit dem Metainformationen zu den Daten hinzugefügt werden. Auch OData und JsonApi Standards einen .

Der große Nachteil (imho) ist, dass die Verarbeitung der Antwortdaten komplexer wird, da die tatsächlichen Daten irgendwo im Umschlag gefunden werden müssen. Es gibt auch viele verschiedene Formate für diesen Umschlag und Sie müssen das richtige verwenden. Es ist bezeichnend, dass die Antwortumschläge von OData und JsonApi sehr unterschiedlich sind, wobei OData an mehreren Stellen in der Antwort Metadaten einmischt.

Separater Endpunkt

Ich denke, dies wurde in den anderen Antworten ausreichend behandelt. Ich habe nicht so viel untersucht, weil ich den Kommentaren zustimme, dass dies verwirrend ist, da Sie jetzt mehrere Arten von Endpunkten haben. Ich finde es am schönsten, wenn jeder Endpunkt eine (Sammlung von) Ressourcen darstellt.

Weitere Gedanken

Wir müssen nicht nur die Paging-Metainformationen in Bezug auf die Antwort kommunizieren, sondern dem Client auch erlauben, bestimmte Seiten / Bereiche anzufordern. Es ist interessant, auch diesen Aspekt zu betrachten, um eine kohärente Lösung zu erhalten. Auch hier können wir Header (der RangeHeader scheint sehr geeignet zu sein) oder andere Mechanismen wie Abfrageparameter verwenden. Einige Leute befürworten die Behandlung von Seiten der Ergebnisse als separate Ressourcen, der Sinn in einigen Anwendungsfällen machen kann (zB /books/231/pages/52. Ich landete eine wilde Reihe von häufig verwendeten Anforderungsparametern Auswahl wie pagesize, page[size]und limitusw. zusätzlich zur Unterstützung der RangeHeader (und als Anforderungsparameter auch).

Stijn de Witt
quelle
Ich war besonders an RangeHeadern interessiert , konnte jedoch nicht genügend Beweise dafür finden, dass die Verwendung von etwas anderem bytesals einem Bereichstyp gültig ist.
VisioN
2
Ich denke, die klarsten Beweise finden sich in Abschnitt 14.5 des RFC : acceptable-ranges = 1#range-unit | "none"Ich denke, diese Formulierung lässt explizit Raum für andere Bereichseinheiten als bytes, obwohl die Spezifikation selbst nur definiert bytes.
Stijn de Witt
24

Alternative, wenn Sie keine tatsächlichen Artikel benötigen

Die Antwort von Franci Penov ist sicherlich der beste Weg, sodass Sie immer Artikel zusammen mit allen zusätzlichen Metadaten zu Ihren angeforderten Entitäten zurückgeben. So sollte es gemacht werden.

Manchmal ist es jedoch nicht sinnvoll, alle Daten zurückzugeben, da Sie sie möglicherweise überhaupt nicht benötigen. Vielleicht brauchen Sie nur diese Metadaten zu Ihrer angeforderten Ressource. Wie Gesamtzahl oder Anzahl der Seiten oder etwas anderes. In diesem Fall kann eine URL-Abfrage Ihren Dienst jederzeit anweisen, keine Elemente zurückzugeben, sondern nur Metadaten wie:

/api/members?metaonly=true
/api/members?includeitems=0

oder etwas ähnliches...

Robert Koritnik
quelle
10
Das Einbetten dieser Informationen in Header hat den Vorteil, dass Sie eine HEAD-Anfrage stellen können, um nur die Anzahl zu erhalten.
Felixfbecker
1
@felixfbecker genau, danke für die Neuerfindung des Rades und die Unordnung der APIs mit allen möglichen Mechanismen :)
EralpB
1
@EralpB Danke, dass du das Rad neu erfunden und die APIs überladen hast!? HEAD ist in HTTP angegeben. metaonlyoder includeitemsnicht.
Felixfbecker
2
@felixfbecker war nur "genau" für dich gedacht, der Rest ist für das OP. Entschuldigung für die Verwirrung.
EralpB
Bei REST geht es darum, HTTP zu nutzen und es so weit wie möglich für das zu nutzen, wofür es gedacht war. In diesem Fall sollte der Inhaltsbereich (RFC7233) verwendet werden. Lösungen im Körper sind nicht gut, vor allem, weil sie mit HEAD nicht funktionieren. Das Erstellen neuer Header, wie hier vorgeschlagen, ist unnötig und falsch.
Vance Shipley
23

Sie können die Anzahl als benutzerdefinierten HTTP-Header als Antwort auf eine HEAD-Anforderung zurückgeben. Auf diese Weise müssen Sie die tatsächliche Liste nicht zurückgeben, wenn ein Client nur die Zählung wünscht, und es ist keine zusätzliche URL erforderlich.

(Wenn Sie sich in einer kontrollierten Umgebung von Endpunkt zu Endpunkt befinden, können Sie ein benutzerdefiniertes HTTP-Verb wie COUNT verwenden.)

bzlm
quelle
4
"Benutzerdefinierter HTTP-Header"? Das würde unter die Überschrift "etwas überraschend" fallen, was wiederum im Widerspruch zu dem steht, was ich für eine RESTful-API halte. Letztendlich sollte es nicht überraschend sein.
Donal Fellows
21
@Donal ich weiß. Aber alle guten Antworten waren bereits vergeben. :(
bzlm
1
Ich weiß es auch, aber manchmal muss man einfach andere Leute antworten lassen. Oder verbessern Sie Ihren Beitrag auf andere Weise, z. B. durch eine ausführliche Erklärung, warum dies am besten und nicht auf andere Weise erfolgen sollte.
Donal Fellows
4
In einer kontrollierten Umgebung könnte dies nicht überraschend sein, da es wahrscheinlich intern verwendet wird und auf der API-Richtlinie Ihrer Entwickler basiert. Ich würde sagen, dies war in einigen Fällen eine gute Lösung und es lohnt sich, sie hier als Hinweis auf eine mögliche ungewöhnliche Lösung zu haben.
James Billingham
1
Ich benutze sehr gerne HTTP-Header für solche Dinge (es ist wirklich dort, wo es hingehört). In diesem Fall ist möglicherweise der Standard- Link-Header geeignet (die Github-API verwendet diesen).
Mike Marcacci
11

Ich würde empfehlen, Header für das gleiche hinzuzufügen, wie:

HTTP/1.1 200

Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json

[
  {
    "id": 10,
    "name": "shirt",
    "color": "red",
    "price": "$23"
  },
  {
    "id": 11,
    "name": "shirt",
    "color": "blue",
    "price": "$25"
  }
]

Einzelheiten finden Sie unter:

https://github.com/adnan-kamili/rest-api-response-format

Für Swagger-Datei:

https://github.com/adnan-kamili/swagger-response-template

Adnan Kamili
quelle
7

Ab "X -" - Präfix war veraltet. (siehe: https://tools.ietf.org/html/rfc6648 )

Wir fanden, dass die "Accept-Ranges" die beste Wahl sind, um den Paginierungsbereich abzubilden: https://tools.ietf.org/html/rfc7233#section-2.3 Da die "Range Units" entweder "Bytes" oder " Zeichen". Beide repräsentieren keinen benutzerdefinierten Datentyp. (siehe: https://tools.ietf.org/html/rfc7233#section-4.2 ) Dennoch wird angegeben, dass

HTTP / 1.1-Implementierungen können Bereiche ignorieren, die mit anderen Einheiten angegeben wurden.

Was darauf hinweist: Die Verwendung von benutzerdefinierten Bereichseinheiten verstößt nicht gegen das Protokoll, kann jedoch ignoriert werden.

Auf diese Weise müssten wir die Accept-Ranges auf "Mitglieder" oder einen beliebigen Typ von Fernkampfeinheiten setzen, den wir erwarten würden. Stellen Sie außerdem den Inhaltsbereich auf den aktuellen Bereich ein. (siehe: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

In jedem Fall würde ich mich an die Empfehlung von RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) halten, eine 206 anstelle von 200 zu senden:

Wenn alle Voraussetzungen erfüllt sind, unterstützt der Server das
Feld Range Header für die Zielressource und die angegebenen Bereiche sind
gültig und erfüllbar (wie in Abschnitt 2.1 definiert). Der Server sollte
eine 206-Antwort ( Teilinhalt) senden mit einer Nutzlast, die eine
oder mehrere Teildarstellungen enthält, die den
angeforderten erfüllbaren Bereichen entsprechen, wie in Abschnitt 4 definiert.

Als Ergebnis hätten wir also die folgenden HTTP-Headerfelder:

Für Teilinhalte:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Für den vollständigen Inhalt:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20
Schmetterling
quelle
3

Scheint am einfachsten, einfach a hinzuzufügen

GET
/api/members/count

und geben Sie die Gesamtzahl der Mitglieder zurück

willcodejavaforfood
quelle
11
Keine gute Idee. Sie verpflichten Kunden, 2 Anfragen zum Erstellen der Paginierung auf ihren Seiten zu stellen. Erste Anforderung, um die Liste der Ressourcen zu erhalten, und zweite, um die Gesamtsumme zu zählen.
Jekis
Ich denke, es ist ein guter Ansatz ... Sie können auch nur eine Liste der Ergebnisse als json zurückgeben und auf der Clientseite die Größe der Sammlung überprüfen, so dass ein solcher Fall ein dummes Beispiel ist ... außerdem können Sie / api / members / count und dann / api haben / Mitglieder? Offset = 10 & Limit = 20
Michał Ziobro
1
Denken Sie auch daran , dass viele Arten von Paginierung nicht eine Zählung (wie unendliche Scrollen) erfordern - Warum dies berechnen , wenn der Kunde es nicht brauchen kann
tofarr
2

Was ist mit einem neuen Endpunkt> / api / members / count, der nur Members.Count () aufruft und das Ergebnis zurückgibt?

Steve Woods
quelle
27
Wenn Sie der Zählung einen expliziten Endpunkt geben, wird sie zu einer eigenständigen adressierbaren Ressource. Es wird funktionieren, wirft jedoch interessante Fragen für jeden auf, der neu in Ihrer API ist. Ist die Anzahl der Sammlungsmitglieder eine separate Ressource von der Sammlung? Kann ich es mit einer PUT-Anfrage aktualisieren? Existiert es für eine leere Sammlung oder nur, wenn sich Elemente darin befinden? Wenn die membersSammlung durch eine POST-Anfrage an erstellt werden kann /api, wird /api/members/countsie auch als Nebeneffekt erstellt, oder muss ich eine explizite POST-Anfrage erstellen, um sie zu erstellen, bevor ich sie anfordere? :-)
Franci Penov
2

Manchmal erfordern Frameworks (wie $ resource / AngularJS) ein Array als Abfrageergebnis, und Sie können nicht wirklich eine Antwort wie erhalten {count:10,items:[...]} in diesem Fall speichere ich "count" in responseHeaders.

PS Eigentlich kann man das mit $ resource / AngularJS machen, aber es braucht einige Verbesserungen.

Vahe Hovhannisyan
quelle
Was sind das für Verbesserungen? Sie wären hilfreich bei Fragen wie dieser: stackoverflow.com/questions/19140017/…
JBCP
Angular erfordert kein Array als Abfrageergebnis, Sie müssen nur Ihre Ressource mit der Option Objekteigenschaft konfigurieren:isArray: false|true
Rémi Becheras
0

Sie könnten countsals Ressource betrachten. Die URL wäre dann:

/api/counts/member
Frank Rem
quelle
-1

Wenn Sie paginierte Daten anfordern, kennen Sie (anhand des expliziten Seitengrößenparameterwerts oder des Standardseitengrößenwerts) die Seitengröße, sodass Sie wissen, ob Sie alle Daten als Antwort erhalten haben oder nicht. Wenn weniger Daten als eine Seitengröße antworten, erhalten Sie ganze Daten. Wenn eine ganze Seite zurückgegeben wird, müssen Sie erneut nach einer anderen Seite fragen.

Ich bevorzuge einen separaten Endpunkt für count (oder denselben Endpunkt mit dem Parameter countOnly). Weil Sie den Endbenutzer auf einen langen / zeitaufwändigen Prozess vorbereiten können, indem Sie den ordnungsgemäß initiierten Fortschrittsbalken anzeigen.

Wenn Sie in jeder Antwort die Datengröße zurückgeben möchten, sollte die Seitengröße und der Offset ebenfalls angegeben sein. Um ehrlich zu sein, ist der beste Weg, auch einen Anforderungsfilter zu wiederholen. Die Reaktion wurde jedoch sehr komplex. Daher bevorzuge ich einen dedizierten Endpunkt, um die Anzahl zurückzugeben.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Couleage von mir, ziehen Sie einen countOnly-Parameter dem vorhandenen Endpunkt vor. Wenn angegeben, enthält die Antwort nur Metadaten.

Endpunkt? Filter = Wert

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

Endpunkt? filter = value & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Wooff
quelle