it-swarm.com.de

Muster für die Behandlung von Batch-Operationen in REST Web-Services?

Welche bewährten Entwurfsmuster gibt es für Batch-Vorgänge für Ressourcen innerhalb eines REST - Webservices?

Ich versuche, ein Gleichgewicht zwischen Idealen und Realität in Bezug auf Leistung und Stabilität zu finden. Wir haben jetzt eine API, in der alle Operationen 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 hier das Äquivalent REST? Oder ist es in Ordnung, ab und zu Kompromisse einzugehen? Ruiniert es das Design, ein paar spezifische Operationen hinzuzufügen, bei denen es die Leistung wirklich verbessert, usw.? Der Client ist derzeit in jedem Fall ein Webbrowser (Javascript-Anwendung auf der Clientseite).

165
Mark Renouf

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

Die Stapelaktualisierung von Teilressourcen oder Ressourcenattributen ist etwas komplizierter. Aktualisieren Sie also jedes markedAsRead-Attribut. Anstatt das Attribut als Teil jeder Ressource zu behandeln, behandeln Sie es im Grunde genommen als einen Eimer, in den Ressourcen gestellt 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 markierten E-Mails.

Sie können dies auch verwenden, um mehrere Artikel derselben Kategorie zuzuordnen.

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

Es ist offensichtlich viel komplizierter, Batch-Teilaktualisierungen im iTunes-Stil durchzuführen (z. B. Interpret + Albumtitel, aber nicht Tracktitel). 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 Unterressource.

POST /mail/0/markAsRead
POSTDATA: true

Alternativ können Sie parametrisierte Ressourcen verwenden. Dies ist in REST -Mustern seltener, in den URI- und HTTP-Spezifikationen jedoch zulässig. Ein Semikolon unterteilt horizontal zusammenhängende Parameter in 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 RESTvolle Kreativität ist im Überfluss vorhanden.

74
Alex

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

Ich erinnere mich an ein Muster aus Cranes und Pascarellos Buch Ajax in Action (übrigens ein exzellentes Buch - sehr zu empfehlen), in dem sie die Implementierung einer CommandQueue veranschaulichen Art von Objekt, dessen Aufgabe es ist, Anforderungen in Stapel einzureihen und sie dann regelmäßig auf dem Server zu veröffentlichen.

Das Objekt enthielt, wenn ich mich richtig erinnere, im Wesentlichen nur eine Reihe von "Befehlen" - um Ihr Beispiel zu erweitern, jeweils einen Datensatz, der einen "markAsRead" -Befehl, eine "messageId" und möglicherweise einen Verweis auf einen Callback/Handler enthielt function - und dann wird das Befehlsobjekt nach einem bestimmten Zeitplan oder bei einer bestimmten Benutzeraktion serialisiert und auf den Server gesendet, und der Client übernimmt die nachfolgende Nachbearbeitung.

Ich habe die Details nicht griffbereit, aber es hört sich so an, als wäre eine solche Befehlswarteschlange eine Möglichkeit, Ihr Problem zu lösen. Dies würde das allgemeine Gesprächsgeschehen erheblich reduzieren und die serverseitige Benutzeroberfläche auf eine Weise abstrahieren, die Sie später möglicherweise flexibler finden würden.


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

Dies ist einfach zu codieren, kann jedoch zu einer Menge sehr kleiner Datenströme zum 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 eine Warteschlange stellen und sie dann nach Belieben stapelweise an den Server senden. Eine einfache, in JavaScript implementierte Aktualisierungswarteschlange finden Sie in Listing 5.13. [...]

Die Warteschlange enthält zwei Arrays. queued ist ein numerisch indiziertes Array, an das neue Aktualisierungen 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, die für das Hinzufügen von Befehlen zur Warteschlange (addCommand) und eine, die für das Serialisieren und anschließende Senden an den Server (fireRequest) verantwortlich ist:

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!

25

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

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

    [GET] mail/1

bedeutet, dass Sie den Datensatz aus der Mail mit der ID 1 und erhalten

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

bedeutet, den Mail-Datensatz mit der ID 1 zu patchen. Die Abfragezeichenfolge 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 diesem Pfad würde also heißen "Patch the records bereits als wahr markieren" ... das ist nicht das, was wir erreichen wollen.

Eine Batch-Methode, die dieser Überlegung 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 wahr ist REST (was keine Manipulation von Chargenprotokollen erlaubt)), sondern es folgt der Logik, die bereits existiert und von REST verwendet wird.

19
fezfox

Ihre Sprache "Es scheint sehr verschwenderisch ..." deutet auf einen Versuch einer vorzeitigen Optimierung hin. Wenn nicht gezeigt werden kann, dass das Senden der gesamten Repräsentation von Objekten eine erhebliche Leistungseinbuße darstellt (dies ist für Benutzer mit einem Zeitintervall von> 150 ms nicht akzeptabel), ist es sinnlos, ein neues, nicht standardmäßiges API-Verhalten zu erstellen. Denken Sie daran, je einfacher die API ist, desto einfacher ist ihre Verwendung.

Für Löschvorgänge senden Sie Folgendes, da der Server nichts über den Status des Objekts wissen muss, bevor der Löschvorgang ausgeführt wird.

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

Der nächste Gedanke ist, dass, wenn eine Anwendung in Bezug auf die Massenaktualisierung von Objekten auf Leistungsprobleme stößt, erwogen werden sollte, jedes Objekt in mehrere Objekte aufzuteilen. Auf diese Weise ist die JSON-Nutzlast nur ein Bruchteil der Größe.

Wenn Sie beispielsweise eine Antwort senden, um den Status "Gelesen" 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 (gelesen, archiviert, wichtig, Labels) 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 PATCHs 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 Leute geben an, dass PATCH implementiert werden soll, indem sie eine Reihe von Änderungen bereitstellen, die Folgendes enthalten: Aktion (CRUD), Pfad (URL) und Wertänderung. Dies kann als Standardimplementierung betrachtet werden, aber wenn Sie sich die Gesamtheit einer REST API ansehen, handelt es sich um eine nicht intuitive einmalige Implementierung. Die obige Implementierung ist auch die von 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.

11
justin.hughey

Die Google Drive API hat ein wirklich interessantes System, um dieses Problem zu lösen ( siehe hier ).

Was sie tun, ist im Grunde verschiedene Anfragen in einem zu gruppieren Content-Type: multipart/mixed request, wobei jede einzelne vollständige Anfrage durch ein definiertes Trennzeichen getrennt ist. Header und Abfrageparameter der Stapelanforderung werden an die einzelnen Anforderungen vererbt (d. H. Authorization: Bearer some_token) es sei denn, sie werden in der Einzelanfrage überschrieben.


Beispiel : (entnommen aus ihrem docs )

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--
7
Aides

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

Es ist nicht viel Mühe, einen Parser zu erstellen, der "messageIds = 1-3,7-9,11,12-15" lesen kann. Es würde sicherlich die Effizienz für pauschale Operationen erhöhen, die alle Nachrichten abdecken, und ist skalierbarer.

1
One Monkey

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 Gruppen-IDs übergeben wird, z.

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

... dann übergebe das an einen WHERE IN-Klausel in meinem SQL. Es funktioniert gut, aber ich frage mich, was andere von diesem Ansatz halten.

1
Roberto

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

Eine einzelne HTTP-Anforderung wird mit einem Batch-Parameter und einer Anforderung für ein Token durchgeführt.

Im Batch wird ein Json gesendet. die 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 Methoden post und put eine "body" -Eigenschaft, in der die Felder aktualisiert werden sind gesendet .

weitere Infos unter: Facebook Batch API

0