Best Practice für Teilaktualisierungen in einem RESTful-Service


208

Ich schreibe einen RESTful-Service für ein Kundenverwaltungssystem und versuche, die beste Vorgehensweise zum teilweisen Aktualisieren von Datensätzen zu finden. Ich möchte beispielsweise, dass der Anrufer den vollständigen Datensatz mit einer GET-Anforderung lesen kann. Zum Aktualisieren sind jedoch nur bestimmte Vorgänge im Datensatz zulässig, z. B. das Ändern des Status von AKTIVIERT in DEAKTIVIERT. (Ich habe komplexere Szenarien als diese)

Ich möchte aus Sicherheitsgründen nicht, dass der Anrufer den gesamten Datensatz nur mit dem aktualisierten Feld übermittelt (es fühlt sich auch wie ein Overkill an).

Gibt es eine empfohlene Methode zum Erstellen der URIs? Beim Lesen der REST-Bücher scheinen RPC-Aufrufe verpönt zu sein.

Wenn der folgende Aufruf den vollständigen Kundendatensatz für den Kunden mit der ID 123 zurückgibt

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>

Wie soll ich den Status aktualisieren?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Update : Um die Frage zu erweitern. Wie integriert man "Business Logic Calls" in eine REST-API? Gibt es einen vereinbarten Weg, dies zu tun? Nicht alle Methoden sind von Natur aus CRUD. Einige sind komplexer, wie " sendEmailToCustomer (123) ", " mergeCustomers (123, 456) ", " countCustomers () ".

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Um Ihre Frage zu "Business Logic Calls" zu beantworten, finden Sie hier einen Beitrag POSTvon Roy Fielding selbst: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post, in dem die Grundidee lautet: Wenn nicht Es handelt sich nicht um eine Methode (wie GEToder PUT), die sich ideal für Ihren Einsatz eignet POST.
Rojoca

Das ist so ziemlich das, was ich letztendlich getan habe. Führen Sie REST-Aufrufe zum Abrufen und Aktualisieren bekannter Ressourcen mit GET, PUT, DELETE durch. POST zum Hinzufügen neuer Ressourcen und POST mit einer beschreibenden URL für Geschäftslogikaufrufe.
Magiconair

Was auch immer Sie sich entscheiden, wenn dieser Vorgang nicht Teil der GET-Antwort ist, haben Sie keinen RESTful-Service. Ich
sehe

Antworten:


69

Sie haben grundsätzlich zwei Möglichkeiten:

  1. Verwenden Sie PATCH(beachten Sie jedoch, dass Sie Ihren eigenen Medientyp definieren müssen, der genau angibt, was passieren wird).

  2. Verwenden Sie POSTdiese Option für eine Unterressource und geben Sie sie zurück. 303 Siehe Andere, wobei der Standortkopf auf die Hauptressource verweist. Mit dem 303 soll dem Client mitgeteilt werden: "Ich habe Ihren POST durchgeführt, und der Effekt war, dass eine andere Ressource aktualisiert wurde. Siehe Standortkopfzeile für welche Ressource dies war." POST / 303 ist für iterative Ergänzungen zu Ressourcen vorgesehen, um den Status einer Hauptressource aufzubauen, und eignet sich perfekt für Teilaktualisierungen.


OK, der POST / 303 macht für mich Sinn. PATCH und MERGE konnte ich nicht in der Liste der gültigen HTTP-Verben finden, sodass weitere Tests erforderlich wären. Wie würde ich eine URI erstellen, wenn das System eine E-Mail an den Kunden 123 senden soll? So etwas wie ein reiner RPC-Methodenaufruf, der den Status des Objekts überhaupt nicht ändert. Was ist der RESTful Weg, dies zu tun?
Magiconair

Ich verstehe die E-Mail-URI-Frage nicht. Möchten Sie ein Gateway implementieren, an das Sie POST senden können, damit es eine E-Mail sendet, oder suchen Sie mailto: customer.123@service.org?
Jan Algermissen

15
Weder REST noch HTTP haben etwas mit CRUD zu tun, abgesehen von einigen Leuten, die die HTTP-Methoden mit CRUD gleichsetzen. Bei REST geht es darum, den Ressourcenzustand durch Übertragen von Darstellungen zu manipulieren. Was auch immer Sie erreichen möchten, Sie tun dies, indem Sie eine Darstellung auf eine Ressource mit der entsprechenden Semantik übertragen. Hüten Sie sich vor den Begriffen "reine Methodenaufrufe" oder "Geschäftslogik", da sie zu leicht "HTTP ist für den Transport" implizieren. Wenn Sie eine E-Mail senden müssen, POST an eine Gateway-Ressource, wenn Sie zu Konten zusammenführen müssen, erstellen Sie eine neue und POST-Darstellungen der beiden anderen usw.
Jan Algermissen

9
Siehe auch, wie Google es macht: googlecode.blogspot.com/2010/03/…
Marius

4
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{"op": "test", "path": "/ a / b / c", "value" : "foo"}, {"op": "remove", "path": "/ a / b / c"}, {"op": "add", "path": "/ a / b / c" , "Wert": ["foo", "bar"]}, {"op": "ersetzen", "Pfad": "/ a / b / c", "Wert": 42}, {"op": "verschieben", "von": "/ a / b / c", "Pfad": "/ a / b / d"}, {"op": "kopieren", "von": "/ a / b / d "," path ":" / a / b / e "}]
intecho

48

Sie sollten POST für Teilaktualisierungen verwenden.

Um Felder für Kunde 123 zu aktualisieren, senden Sie einen POST an / customer / 123.

Wenn Sie nur den Status aktualisieren möchten, können Sie auch auf / customer / 123 / status setzen.

Im Allgemeinen sollten GET-Anforderungen keine Nebenwirkungen haben, und PUT dient zum Schreiben / Ersetzen der gesamten Ressource.

Dies folgt direkt aus HTTP, wie hier zu sehen: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods


1
@ John Saunders POST muss nicht unbedingt eine neue Ressource erstellen , die von einer URI zugänglich ist: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: Ich weiß, dass es nicht zu einer neuen URL führen muss, dachte aber trotzdem, dass ein POST /customer/123das Offensichtliche schaffen sollte, das logischerweise unter Kunde 123 liegt. Vielleicht eine Bestellung? PUT to /customer/123/statusscheint sinnvoller zu sein, vorausgesetzt, der POST /customerserstellt implizit ein status(und nimmt an, dass dies ein legitimer REST ist).
John Saunders

1
@ John Saunders: Wenn wir ein Feld in einer Ressource aktualisieren möchten, die sich an einem bestimmten URI befindet, ist POST praktisch sinnvoller als PUT. Da kein UPDATE vorhanden ist, wird es meiner Meinung nach häufig in REST-Services verwendet. POST an / Kunden kann einen neuen Kunden erstellen, und ein PUT an / Kunde / 123 / Status kann besser mit dem Wort der Spezifikation übereinstimmen, aber was Best Practices betrifft, gibt es meines Erachtens keinen Grund, nicht an / POST zu senden customer / 123, um ein Feld zu aktualisieren - es ist präzise, ​​sinnvoll und widerspricht nicht strikt den Angaben in der Spezifikation.
Wsorenson

8
Sollten POST-Anfragen nicht idempotent sein? Sicherlich ist das Aktualisieren eines Eintrags idempotent und sollte daher stattdessen ein PUT sein?
Martin Andersson

1
@MartinAndersson POST-requests nicht brauchen nicht idempotent zu sein. Und wie gesagt, PUTmuss eine ganze Ressource ersetzen.
Halle Knast

10

Sie sollten PATCH für Teilaktualisierungen verwenden - entweder mithilfe von Json-Patch-Dokumenten (siehe http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 oder http://www.mnot.net/). blog / 2012/09/05 / patch ) oder das XML-Patch-Framework (siehe http://tools.ietf.org/html/rfc5261 ). Meiner Meinung nach passt json-patch am besten zu Ihrer Art von Geschäftsdaten.

PATCH mit JSON / XML-Patchdokumenten bietet eine sehr direkte Semantik für Teilaktualisierungen. Wenn Sie POST mit geänderten Kopien des Originaldokuments für Teilaktualisierungen verwenden, treten bald Probleme auf, bei denen fehlende Werte (oder vielmehr Nullwerte) entweder "Diese Eigenschaft ignorieren" oder "Diese Eigenschaft auf" setzen sollen leerer Wert "- und das führt zu einem Hasenloch gehackter Lösungen, die am Ende zu Ihrem eigenen Patch-Format führen.

Eine ausführlichere Antwort finden Sie hier: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .


Bitte beachten Sie, dass die RFCs für json-patch und xml-patch inzwischen finalisiert wurden.
Botchniaque

8

Ich habe ein ähnliches Problem. PUT für eine Unterressource scheint zu funktionieren, wenn Sie nur ein einzelnes Feld aktualisieren möchten. Manchmal möchten Sie jedoch eine Reihe von Dingen aktualisieren: Stellen Sie sich ein Webformular vor, das die Ressource mit der Option zum Ändern einiger Einträge darstellt. Die Übermittlung des Formulars durch den Benutzer sollte nicht zu mehreren PUTs führen.

Hier sind zwei Lösungen, die mir einfallen:

  1. Führen Sie einen PUT mit der gesamten Ressource durch. Definieren Sie auf der Serverseite die Semantik, nach der ein PUT mit der gesamten Ressource alle Werte ignoriert, die sich nicht geändert haben.

  2. Führen Sie einen PUT mit einer Teilressource durch. Definieren Sie auf der Serverseite die Semantik als Zusammenführung.

2 ist nur eine Bandbreitenoptimierung von 1. Manchmal ist 1 die einzige Option, wenn die Ressource definiert, dass einige Felder Pflichtfelder sind (denken Sie an Protopuffer).

Das Problem bei beiden Ansätzen ist, wie ein Feld gelöscht wird. Sie müssen einen speziellen Nullwert definieren (insbesondere für Protopuffer, da für Protopuffer keine Nullwerte definiert sind), der zum Löschen des Felds führt.

Bemerkungen?


2
Dies wäre nützlicher, wenn es als separate Frage gestellt würde.
Intecho

6

Zum Ändern des Status besteht meines Erachtens ein RESTful-Ansatz darin, eine logische Unterressource zu verwenden, die den Status der Ressourcen beschreibt. Diese IMO ist sehr nützlich und sauber, wenn Sie weniger Status haben. Dadurch wird Ihre API aussagekräftiger, ohne dass die vorhandenen Vorgänge für Ihre Kundenressource erzwungen werden müssen.

Beispiel:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}

Der POST-Service sollte den neu erstellten Kunden mit der folgenden ID zurückgeben:

{
    id:123,
    ...  // the other fields here
}

Das GET für die erstellte Ressource würde den Ressourcenspeicherort verwenden:

GET /customer/123/active

Ein GET / customer / 123 / inactive sollte 404 zurückgeben

Bei der PUT-Operation wird ohne Angabe einer Json-Entität lediglich der Status aktualisiert

PUT /customer/123/inactive  <-- Deactivating an existing customer

Durch die Bereitstellung einer Entität können Sie den Inhalt des Kunden aktualisieren und gleichzeitig den Status aktualisieren.

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}

Sie erstellen eine konzeptionelle Unterressource für Ihre Kundenressource. Dies steht auch im Einklang mit Roy Fieldings Definition einer Ressource: "... Eine Ressource ist eine konzeptionelle Zuordnung zu einer Reihe von Entitäten, nicht die Entität, die der Zuordnung zu einem bestimmten Zeitpunkt entspricht ..." In diesem Fall ist die Die konzeptionelle Zuordnung erfolgt von Kunde zu Kunde mit dem Status = AKTIV.

Lesevorgang:

GET /customer/123/active 
GET /customer/123/inactive

Wenn Sie diese Anrufe direkt nach dem anderen tätigen, muss einer von ihnen den Status 404 zurückgeben, enthält die erfolgreiche Ausgabe möglicherweise nicht den impliziten Status. Natürlich können Sie weiterhin GET / customer / 123? Status = ACTIVE | INACTIVE verwenden, um die Kundenressource direkt abzufragen.

Die DELETE-Operation ist interessant, da die Semantik verwirrend sein kann. Sie haben jedoch die Möglichkeit, diesen Vorgang für diese konzeptionelle Ressource nicht zu veröffentlichen oder gemäß Ihrer Geschäftslogik zu verwenden.

DELETE /customer/123/active

Dieser kann Ihren Kunden in den Status DELETED / DISABLED oder in den entgegengesetzten Status (ACTIVE / INACTIVE) versetzen.


Wie gelangen Sie zur Unterressource?
MStodd

Ich überarbeitete die Antwort, um es klarer zu machen
raspacorp

5

Dinge, die Sie Ihrer erweiterten Frage hinzufügen sollten. Ich denke, Sie können oft kompliziertere Geschäftsaktionen perfekt gestalten. Aber Sie müssen den Methoden- / Verfahrensstil des Denkens verraten und mehr in Ressourcen und Verben denken.

Mailversand


POST /customers/123/mails

payload:
{from: x@x.com, subject: "foo", to: y@y.com}

Die Implementierung dieser Ressource + POST würde dann die Mail senden. Bei Bedarf können Sie dann etwas wie / customer / 123 / outbox anbieten und dann Ressourcenlinks zu / customer / mails / {mailId} anbieten.

Kundenanzahl

Sie können damit wie mit einer Suchressource umgehen (einschließlich Suchmetadaten mit Paging und num-found-Informationen, mit denen Sie die Anzahl der Kunden angeben können).


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}


Ich mag die Art der logischen Gruppierung von Feldern in der POST-Unterressource.
Gertas

3

Verwenden Sie PUT zum Aktualisieren unvollständiger / teilweiser Ressourcen.

Sie können jObject als Parameter akzeptieren und seinen Wert analysieren, um die Ressource zu aktualisieren.

Nachfolgend finden Sie die Funktion, die Sie als Referenz verwenden können:

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    foreach (JProperty property in json.Properties())
    {
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    }

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);            
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);
}

2

In Bezug auf Ihr Update.

Ich glaube, das Konzept von CRUD hat einige Verwirrung hinsichtlich des API-Designs verursacht. CRUD ist ein allgemeines Low-Level-Konzept für grundlegende Operationen, die mit Daten ausgeführt werden sollen, und HTTP-Verben sind lediglich Anforderungsmethoden ( vor 21 Jahren erstellt ), die einer CRUD-Operation zugeordnet werden können oder nicht. Versuchen Sie tatsächlich, das Vorhandensein des Akronyms CRUD in der HTTP 1.0 / 1.1-Spezifikation zu finden.

Eine sehr gut erläuterte Anleitung, die eine pragmatische Konvention anwendet, finden Sie in der API-Dokumentation zur Google Cloud-Plattform . Es beschreibt die Konzepte hinter der Erstellung einer ressourcenbasierten API, die eine große Menge an Ressourcen gegenüber Vorgängen hervorhebt, und enthält die von Ihnen beschriebenen Anwendungsfälle. Obwohl es sich nur um ein Kongressdesign für ihr Produkt handelt, halte ich es für sehr sinnvoll.

Das Grundkonzept hier (und eines, das viel Verwirrung stiftet) ist die Zuordnung zwischen "Methoden" und HTTP-Verben. Eine Sache ist zu definieren, welche "Operationen" (Methoden) Ihre API über welche Arten von Ressourcen ausführt (z. B. eine Kundenliste abrufen oder eine E-Mail senden), und eine andere sind die HTTP-Verben. Es muss eine Definition sowohl der Methoden als auch der Verben geben, die Sie verwenden möchten, und eine Zuordnung zwischen ihnen .

Er sagt auch , dass, wenn eine Operation nicht genau mit einem Standardverfahren abbildet ( List, Get, Create, Update, Deletein diesem Fall), kann eine „Custom - Methoden“, wie verwenden BatchGet, die mehrere Objekte - ID Eingabe mehrere Objekte abruft , basierend auf oder SendEmail.


2

RFC 7396 : JSON Merge Patch (veröffentlicht vier Jahre nach Veröffentlichung der Frage) beschreibt die Best Practices für einen PATCH in Bezug auf Format und Verarbeitungsregeln.

Kurz gesagt, Sie senden einen HTTP-PATCH an eine Zielressource mit dem Medientyp application / merge-patch + json MIME und einem Body, der nur die Teile darstellt, die Sie ändern / hinzufügen / entfernen möchten, und befolgen dann die folgenden Verarbeitungsregeln.

Regeln :

  • Wenn der bereitgestellte Zusammenführungs-Patch Mitglieder enthält, die nicht im Ziel angezeigt werden, werden diese Mitglieder hinzugefügt.

  • Wenn das Ziel das Mitglied enthält, wird der Wert ersetzt.

  • Nullwerte im Zusammenführungs-Patch erhalten eine besondere Bedeutung, um das Entfernen vorhandener Werte im Ziel anzuzeigen.

Beispiel für Testfälle, die die obigen Regeln veranschaulichen (wie im Anhang dieses RFC zu sehen):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}

1

Überprüfen Sie http://www.odata.org/

Es definiert die MERGE-Methode, also wäre es in Ihrem Fall ungefähr so:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Nur die statusEigenschaft wird aktualisiert und die anderen Werte bleiben erhalten.


Ist MERGEein gültiges HTTP-Verb?
John Saunders

3
Schauen Sie sich PATCH an - das ist bald Standard-HTTP und macht das Gleiche.
Jan Algermissen

@ John Saunders Ja, es ist eine Erweiterungsmethode.
Max Toro

FYI MERGE wurde aus OData v4 entfernt. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Siehe docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k

0

Es spielt keine Rolle. In Bezug auf REST können Sie kein GET ausführen, da es nicht zwischengespeichert werden kann. Es spielt jedoch keine Rolle, ob Sie POST oder PATCH oder PUT oder was auch immer verwenden, und es spielt keine Rolle, wie die URL aussieht. Wenn Sie REST ausführen, ist es wichtig, dass diese Darstellung den Clientstatusübergangsoptionen bietet, wenn Sie eine Darstellung Ihrer Ressource vom Server erhalten.

Wenn Ihre GET-Antwort Statusübergänge hatte, muss der Client nur wissen, wie er sie liest, und der Server kann sie bei Bedarf ändern. Hier wird ein Update mit POST durchgeführt. Wenn es jedoch in PATCH geändert wurde oder wenn sich die URL ändert, weiß der Client immer noch, wie ein Update durchgeführt wird:

{
  "customer" :
  {
  },
  "operations":
  [
    "update" : 
    {
      "method": "POST",
      "href": "https://server/customer/123/"
    }]
}

Sie können sogar die erforderlichen / optionalen Parameter auflisten, die der Client an Sie zurückgeben kann. Das hängt von der Anwendung ab.

Für den Geschäftsbetrieb ist dies möglicherweise eine andere Ressource als die Kundenressource. Wenn Sie eine E-Mail an den Kunden senden möchten, handelt es sich bei diesem Service möglicherweise um eine eigene Ressource, an die Sie POSTEN können. Daher können Sie den folgenden Vorgang in die Kundenressource aufnehmen:

"email":
{
  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"
}

Einige gute Videos und ein Beispiel für die REST-Architektur des Präsentators sind diese. Stormpath verwendet nur GET / POST / DELETE, was in Ordnung ist, da REST nichts damit zu tun hat, welche Operationen Sie verwenden oder wie URLs aussehen sollten (außer GETs sollten zwischenspeicherbar sein):

https://www.youtube.com/watch?v=pspy1H6A3FM ,
https://www.youtube.com/watch?v=5WXYw4J4QOU ,
http://docs.stormpath.com/rest/quickstart/

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.