Muster für die Verarbeitung von Stapeloperationen in REST-Webdiensten?

170

Welche bewährten Entwurfsmuster gibt es für Stapeloperationen an Ressourcen innerhalb eines Webdienstes im REST-Stil?

Ich versuche, ein Gleichgewicht zwischen Idealen und Realität in Bezug auf Leistung und Stabilität zu finden. Wir haben derzeit eine API, in der alle Vorgänge entweder von einer Listenressource (dh GET / user) oder von einer einzelnen Instanz (PUT / user / 1, DELETE / user / 22 usw.) abgerufen werden.

In einigen Fällen möchten Sie ein einzelnes Feld einer ganzen Reihe von Objekten aktualisieren. Es erscheint sehr verschwenderisch, die gesamte Darstellung für jedes Objekt hin und her zu senden, um das eine Feld zu aktualisieren.

In einer API im RPC-Stil könnten Sie eine Methode haben:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc. 

Was ist das REST-Äquivalent hier? Oder ist es in Ordnung, ab und zu Kompromisse einzugehen? Ruiniert es das Design, einige spezifische Vorgänge hinzuzufügen, bei denen die Leistung usw. wirklich verbessert wird? Der Client ist derzeit in allen Fällen ein Webbrowser (Javascript-Anwendung auf der Clientseite).

Mark Renouf
quelle

Antworten:

77

Ein einfaches RESTful-Muster für Stapel besteht darin, eine Sammlungsressource zu verwenden. Zum Beispiel, um mehrere Nachrichten gleichzeitig zu löschen.

DELETE /mail?&id=0&id=1&id=2

Es ist etwas komplizierter, Teilressourcen oder Ressourcenattribute stapelweise zu aktualisieren. Aktualisieren Sie also jedes markierte AsRead-Attribut. Anstatt das Attribut als Teil jeder Ressource zu behandeln, behandeln Sie es grundsätzlich als einen Bucket, in den Ressourcen eingefügt werden sollen. Ein Beispiel wurde bereits veröffentlicht. Ich habe es ein wenig angepasst.

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

Grundsätzlich aktualisieren Sie die Liste der als gelesen gekennzeichneten E-Mails.

Sie können dies auch verwenden, um mehrere Elemente derselben Kategorie zuzuweisen.

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

Es ist offensichtlich viel komplizierter, Batch-Teilaktualisierungen im iTunes-Stil durchzuführen (z. B. Artist + AlbumTitle, aber nicht TrackTitle). Die Bucket-Analogie beginnt zusammenzubrechen.

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

Auf lange Sicht ist es viel einfacher, eine einzelne Teilressource oder Ressourcenattribute zu aktualisieren. Nutzen Sie einfach eine Subressource.

POST /mail/0/markAsRead
POSTDATA: true

Alternativ können Sie parametrisierte Ressourcen verwenden. Dies ist in REST-Mustern weniger häufig, in den URI- und HTTP-Spezifikationen jedoch zulässig. Ein Semikolon teilt horizontal verwandte Parameter innerhalb einer Ressource.

Aktualisieren Sie mehrere Attribute, mehrere Ressourcen:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Aktualisieren Sie mehrere Ressourcen, nur ein Attribut:

POST /mail/0;1;2/markAsRead
POSTDATA: true

Aktualisieren Sie mehrere Attribute, nur eine Ressource:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Die RESTful Kreativität ist im Überfluss vorhanden.

Alex
quelle
1
Man könnte argumentieren, dass Ihr Löschen eigentlich ein Beitrag sein sollte, da es diese Ressource nicht wirklich zerstört.
Chris Nicola
6
Es ist nicht notwendig. POST ist eine Factory-Pattern-Methode, die weniger explizit und offensichtlich ist als PUT / DELETE / GET. Die einzige Erwartung ist, dass der Server aufgrund des POST entscheidet, was zu tun ist. POST ist genau das, was es immer war, ich sende Formulardaten und der Server tut etwas (hoffentlich erwartet) und gibt mir einen Hinweis auf das Ergebnis. Wir müssen keine Ressourcen mit POST erstellen, wir entscheiden uns nur oft dafür. Ich kann mit PUT problemlos eine Ressource erstellen. Ich muss lediglich die Ressourcen-URL als Absender definieren (nicht oft ideal).
Chris Nicola
1
@nishant, in diesem Fall müssen Sie wahrscheinlich nicht auf mehrere Ressourcen in der URI verweisen, sondern müssen lediglich Tupel mit den Referenzen / Werten im Hauptteil der Anforderung übergeben. Beispiel: POST / mail / markAsRead, BODY: i_0_id = 0 & i_0_value = true & i_1_id = 1 & i_1_value = false & i_2_id = 2 & i_2_value = true
Alex
3
Semikolon ist für diesen Zweck reserviert.
Alex
1
Überrascht, dass niemand darauf hingewiesen hat, dass das Aktualisieren mehrerer Attribute auf einer einzelnen Ressource gut abgedeckt ist PATCH- in diesem Fall ist keine Kreativität erforderlich.
LB2
25

Überhaupt nicht - ich denke, das REST-Äquivalent ist (oder zumindest eine Lösung ist) fast genau das - eine spezielle Schnittstelle, die für eine vom Client geforderte Operation ausgelegt ist.

Ich erinnere mich an ein Muster, das in Crane und Pascarellos Buch Ajax in Action erwähnt wurde (übrigens ein ausgezeichnetes Buch - sehr zu empfehlen), in dem sie die Implementierung einer CommandQueue veranschaulichen Objekts dessen Aufgabe es ist, Anforderungen in Stapel und in eine Warteschlange zu stellen Dann poste sie regelmäßig auf dem Server.

Wenn ich mich richtig erinnere, enthielt das Objekt im Wesentlichen nur ein Array von "Befehlen" - z. B. um Ihr Beispiel zu erweitern, jedes einen Datensatz, der einen "markAsRead" -Befehl, eine "messageId" und möglicherweise einen Verweis auf einen Rückruf / Handler enthält Funktion - und dann würde das Befehlsobjekt gemäß einem Zeitplan oder einer Benutzeraktion serialisiert und auf den Server gebucht, und der Client würde die konsequente Nachbearbeitung übernehmen.

Ich habe die Details nicht zur Hand, aber es klingt so, als ob eine Befehlswarteschlange dieser Art eine Möglichkeit wäre, Ihr Problem zu lösen. Dies würde die allgemeine Chattiness erheblich reduzieren und die serverseitige Benutzeroberfläche auf eine Weise abstrahieren, die Sie später möglicherweise flexibler finden.


Update : Aha! Ich habe online einen Ausschnitt aus diesem Buch gefunden, der Codebeispiele enthält (obwohl ich immer noch vorschlage, das eigentliche Buch abzuholen!). Schauen Sie hier , beginnend mit Abschnitt 5.5.3:

Dies ist einfach zu codieren, kann jedoch zu sehr kleinen Datenmengen auf dem Server führen, was ineffizient und möglicherweise verwirrend ist. Wenn wir unseren Datenverkehr kontrollieren möchten, können wir diese Aktualisierungen erfassen und lokal in die Warteschlange stellen und sie dann nach Belieben stapelweise an den Server senden. Eine einfache in JavaScript implementierte Aktualisierungswarteschlange ist in Listing 5.13 dargestellt. [...]

Die Warteschlange verwaltet zwei Arrays. queued ist ein numerisch indiziertes Array, an das neue Updates angehängt werden. sent ist ein assoziatives Array, das die Aktualisierungen enthält, die an den Server gesendet wurden, aber auf eine Antwort warten.

Hier sind zwei relevante Funktionen - eine für das Hinzufügen von Befehlen zur Warteschlange ( addCommand) und eine für das Serialisieren und anschließende Senden an den Server ( fireRequest):

CommandQueue.prototype.addCommand = function(command)
{ 
    if (this.isCommand(command))
    {
        this.queue.append(command,true);
    }
}

CommandQueue.prototype.fireRequest = function()
{
    if (this.queued.length == 0)
    { 
        return; 
    }

    var data="data=";

    for (var i = 0; i < this.queued.length; i++)
    { 
        var cmd = this.queued[i]; 
        if (this.isCommand(cmd))
        {
            data += cmd.toRequestString(); 
            this.sent[cmd.id] = cmd;

            // ... and then send the contents of data in a POST request
        }
    }
}

Das sollte dich zum Laufen bringen. Viel Glück!

Christian Nuntius
quelle
Vielen Dank. Das ist meinen Vorstellungen sehr ähnlich, wie ich vorgehen würde, wenn wir die Batch-Operationen auf dem Client belassen würden. Das Problem ist die Umlaufzeit für die Ausführung einer Operation an einer großen Anzahl von Objekten.
Mark Renouf
Hm, ok - Ich dachte, Sie wollten den Vorgang für eine große Anzahl von Objekten (auf dem Server) über eine einfache Anfrage ausführen. Habe ich falsch verstanden?
Christian Nunciato
Ja, aber ich sehe nicht, wie dieses Codebeispiel die Operation effizienter ausführen würde. Es stapelt Anforderungen, sendet sie jedoch nacheinander an den Server. Interpretiere ich falsch?
Mark Renouf
Tatsächlich werden sie gestapelt und dann alle auf einmal gesendet: Die for-Schleife in fireRequest () sammelt im Wesentlichen alle ausstehenden Befehle und serialisiert sie als Zeichenfolge (mit .toRequestString (), z. B. "method = markAsRead & messageIds = 1,2,3" , 4 ") weist diese Zeichenfolge" Daten "zu und POSTs Daten an den Server.
Christian Nunciato
20

Während ich denke, dass @Alex auf dem richtigen Weg ist, denke ich, dass es konzeptionell das Gegenteil von dem sein sollte, was vorgeschlagen wird.

Die URL ist in der Tat "die Ressourcen, auf die wir abzielen", daher:

    [GET] mail/1

bedeutet, den Datensatz von Mail mit ID 1 und zu erhalten

    [PATCH] mail/1 data: mail[markAsRead]=true

bedeutet, den Mail-Datensatz mit der ID 1 zu patchen. Der Querystring ist ein "Filter", der die von der URL zurückgegebenen Daten filtert.

    [GET] mail?markAsRead=true

Hier fordern wir also alle E-Mails an, die bereits als gelesen markiert sind. Zu [PATCH] auf diesen Pfad zu gehen würde also bedeuten, "die bereits als wahr markierten Datensätze zu patchen " ... was wir nicht erreichen wollen.

Eine Batch-Methode, die diesem Gedanken folgt, sollte also sein:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

Natürlich sage ich nicht, dass dies echtes REST ist (was keine Manipulation von Batch-Datensätzen erlaubt), sondern es folgt der Logik, die bereits existiert und von REST verwendet wird.

Fezfox
quelle
Interessante Antwort! Wäre es für Ihr letztes Beispiel nicht konsistenter mit dem [GET]zu erledigenden Format [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](oder sogar nur data: {"ids": [1,2,3]})? Ein weiterer Vorteil dieses alternativen Ansatzes besteht darin, dass beim Aktualisieren von Hunderten / Tausenden von Ressourcen in der Sammlung keine Fehler "414 Request URI too long" auftreten.
Rinogo
@rinogo - eigentlich nein. Dies ist der Punkt, den ich gemacht habe. Der Querystring ist ein Filter für die Datensätze, auf die wir reagieren möchten (z. B. [GET] mail / 1 erhält den Mail-Datensatz mit der ID 1, während [GET] mail? MarkasRead = true Mail zurückgibt, bei der markAsRead bereits true ist). Es macht keinen Sinn, auf dieselbe URL zu patchen (dh "Datensätze mit markAsRead = true patchen"), wenn wir tatsächlich bestimmte Datensätze mit den IDs 1,2,3 patchen möchten, unabhängig vom aktuellen Status des Felds markAsRead. Daher die von mir beschriebene Methode. Stimmen Sie zu, dass beim Aktualisieren vieler Datensätze ein Problem vorliegt. Ich würde einen weniger eng gekoppelten Endpunkt erstellen.
Fezfox
11

Ihre Sprache "Es scheint sehr verschwenderisch ..." deutet auf einen Versuch einer vorzeitigen Optimierung hin. Wenn nicht gezeigt werden kann, dass das Senden der gesamten Darstellung von Objekten einen großen Leistungseinbruch darstellt (wir sprechen von Benutzern als> 150 ms inakzeptabel), macht es keinen Sinn, ein neues nicht standardmäßiges API-Verhalten zu erstellen. Denken Sie daran, je einfacher die API ist, desto einfacher ist ihre Verwendung.

Senden Sie zum Löschen Folgendes, da der Server vor dem Löschen nichts über den Status des Objekts wissen muss.

DELETE /emails
POSTDATA: [{id:1},{id:2}]

Der nächste Gedanke ist, dass, wenn bei einer Anwendung Leistungsprobleme in Bezug auf die Massenaktualisierung von Objekten auftreten, überlegt werden sollte, jedes Objekt in mehrere Objekte aufzuteilen. Auf diese Weise ist die JSON-Nutzlast ein Bruchteil der Größe.

Wenn Sie beispielsweise eine Antwort senden, um den Status "Lesen" und "Archiviert" von zwei separaten E-Mails zu aktualisieren, müssen Sie Folgendes senden:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe!",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
              read:true,
              archived:true,
              importance:2,
              labels:["Someone","Mustard"]
            },
            {
              id:2,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe (With Fix)",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
              read:true,
              archived:false,
              importance:1,
              labels:["Someone","Mustard"]
            }
            ]

Ich würde die veränderlichen Komponenten der E-Mail (Lesen, Archivieren, Wichtigkeit, Beschriftungen) in ein separates Objekt aufteilen, da die anderen (zu, von, Betreff, Text) niemals aktualisiert würden.

PUT /email-statuses
POSTDATA: [
            {id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
            {id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
          ]

Ein weiterer Ansatz besteht darin, die Verwendung eines PATCH zu nutzen. Um explizit anzugeben, welche Eigenschaften Sie aktualisieren möchten und dass alle anderen ignoriert werden sollen.

PATCH /emails
POSTDATA: [
            {
              id:1,
              read:true,
              archived:true
            },
            {
              id:2,
              read:true,
              archived:false
            }
          ]

Die Benutzer geben an, dass PATCH implementiert werden soll, indem eine Reihe von Änderungen bereitgestellt werden, die Folgendes enthalten: Aktion (CRUD), Pfad (URL) und Wertänderung. Dies kann als Standardimplementierung betrachtet werden. Wenn Sie sich jedoch die Gesamtheit einer REST-API ansehen, handelt es sich um eine nicht intuitive Einzelimplementierung. Mit der obigen Implementierung hat GitHub PATCH implementiert .

Zusammenfassend lässt sich sagen, dass es möglich ist, RESTful-Prinzipien mit Batch-Aktionen einzuhalten und dennoch eine akzeptable Leistung zu erzielen.

justin.hughey
quelle
Ich bin damit einverstanden, dass PATCH am sinnvollsten ist. Das Problem ist, dass die Implementierung als einfacher PATCH schwieriger wird, wenn Sie einen anderen Statusübergangscode haben, der ausgeführt werden muss, wenn sich diese Eigenschaften ändern. Ich denke nicht, dass REST wirklich irgendeine Art von Zustandsübergang zulässt, da es zustandslos sein soll, es ist ihm egal, von was es zu und von wechselt, nur was sein aktueller Zustand ist.
BeniRose
Hey BeniRose, danke, dass du einen Kommentar hinzugefügt hast. Ich frage mich oft, ob die Leute einige dieser Beiträge sehen. Es freut mich zu sehen, dass es Menschen tun. Ressourcen bezüglich der "zustandslosen" Natur von REST definieren es als ein Problem, dass der Server den Status nicht über Anforderungen hinweg aufrechterhalten muss. Daher ist mir nicht klar, welches Problem Sie beschrieben haben. Können Sie dies anhand eines Beispiels erläutern?
justin.hughey
8

Die Google Drive API verfügt über ein wirklich interessantes System zur Lösung dieses Problems ( siehe hier ).

Grundsätzlich gruppieren sie verschiedene Anforderungen in einer Content-Type: multipart/mixedAnforderung, wobei jede einzelne vollständige Anforderung durch ein definiertes Trennzeichen getrennt ist. Header und Abfrageparameter der Stapelanforderung werden an die einzelnen Anforderungen (dh Authorization: Bearer some_token) vererbt, sofern sie nicht in der einzelnen Anforderung überschrieben werden.


Beispiel : (aus ihren Dokumenten entnommen )

Anfrage:

POST https://www.googleapis.com/batch

Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963

--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"[email protected]",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
  "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

Antwort:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT

--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "12218244892818058021i"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "04109509152946699072k"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk--
Helfer
quelle
1

Ich wäre in einer Operation wie der in Ihrem Beispiel versucht, einen Bereichsparser zu schreiben.

Es ist nicht sehr mühsam, einen Parser zu erstellen, der "messageIds = 1-3,7-9,11,12-15" lesen kann. Dies würde sicherlich die Effizienz für pauschale Vorgänge erhöhen, die alle Nachrichten abdecken, und ist skalierbarer.


quelle
Gute Beobachtung und eine gute Optimierung, aber die Frage war, ob dieser Anforderungsstil jemals mit dem REST-Konzept "kompatibel" sein könnte.
Mark Renouf
Hi, ja ich verstehe. Die Optimierung macht das Konzept ruhiger und ich wollte meinen Rat nicht auslassen, nur weil es ein kleines Stück vom Thema entfernt war.
1

Guter Eintrag. Ich habe seit ein paar Tagen nach einer Lösung gesucht. Ich habe eine Lösung gefunden, bei der eine Abfragezeichenfolge mit durch Kommas getrennten Bündel-IDs übergeben wird, z.

DELETE /my/uri/to/delete?id=1,2,3,4,5

... dann übergebe das an eine WHERE INKlausel in meinem SQL. Es funktioniert großartig, aber fragen Sie sich, was andere über diesen Ansatz denken.

Roberto
quelle
1
Ich mag es nicht wirklich, weil es einen neuen Typ einführt, die Zeichenfolge, die Sie als Liste verwenden, in der Sie sich befinden. Ich würde sie stattdessen lieber auf einen sprachspezifischen Typ analysieren, und dann kann ich dieselbe Methode in der verwenden gleiche Weise in mehreren verschiedenen Teilen des Systems.
Softarn
4
Eine Erinnerung daran, bei SQL-Injection-Angriffen vorsichtig zu sein und Ihre Daten immer zu bereinigen und Bindungsparameter zu verwenden, wenn Sie diesen Ansatz wählen.
justin.hughey
2
Hängt vom gewünschten Verhalten ab, DELETE /books/delete?id=1,2,3wenn Buch Nr. 3 nicht existiert - das WHERE INignoriert stillschweigend Datensätze, wohingegen ich normalerweise DELETE /books/delete?id=3404 erwarten würde , wenn 3 nicht existiert.
Chbrown
3
Ein anderes Problem, auf das Sie bei der Verwendung dieser Lösung stoßen können, ist die Beschränkung der in einer URL-Zeichenfolge zulässigen Zeichen. Wenn jemand beschließt, 5.000 Datensätze in großen Mengen zu löschen, lehnt der Browser die URL möglicherweise ab, oder der HTTP-Server (z. B. Apache) lehnt sie möglicherweise ab. Die allgemeine Regel (die sich hoffentlich mit besseren Servern und Software ändert) war, mit einer maximalen Größe von 2 KB zu arbeiten. Wo mit dem Körper eines POST können Sie bis zu 10 MB gehen. stackoverflow.com/questions/2364840/…
justin.hughey
0

Aus meiner Sicht denke ich, dass Facebook die beste Implementierung hat.

Eine einzelne HTTP-Anforderung wird mit einem Stapelparameter und einer für ein Token gestellt.

Im Batch wird ein JSON gesendet. welches eine Sammlung von "Anfragen" enthält. Jede Anforderung verfügt über eine Methodeneigenschaft (get / post / put / delete / etc ...) und eine relative_url-Eigenschaft (uri des Endpunkts). Zusätzlich ermöglichen die post- und put-Methoden eine "body" -Eigenschaft, in der die Felder aktualisiert werden sind gesendet .

Weitere Infos unter: Facebook Batch API

Leonardo Jauregui
quelle