Meilleure pratique pour les mises à jour partielles dans un service RESTful


208

J'écris un service RESTful pour un système de gestion client et j'essaie de trouver la meilleure pratique pour mettre à jour les enregistrements partiellement. Par exemple, je veux que l'appelant puisse lire l'enregistrement complet avec une demande GET. Mais pour le mettre à jour, seules certaines opérations sur l'enregistrement sont autorisées, comme changer l'état de ENABLED à DISABLED. (J'ai des scénarios plus complexes que cela)

Je ne veux pas que l'appelant soumette l'intégralité de l'enregistrement avec uniquement le champ mis à jour pour des raisons de sécurité (cela semble également exagéré).

Existe-t-il un moyen recommandé de construire les URI? Lors de la lecture des livres REST, les appels de style RPC semblent être désapprouvés.

Si l'appel suivant renvoie l'enregistrement client complet pour le client avec l'ID 123

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

comment dois-je mettre à jour le statut?

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

POST /customer/123/changeStatus
DISABLED

...

Mise à jour : pour augmenter la question. Comment intégrer des «appels de logique métier» dans une API REST? Existe-t-il une façon convenue de procéder? Toutes les méthodes ne sont pas CRUD par nature. Certains sont plus complexes, comme « sendEmailToCustomer (123) », « mergeCustomers (123, 456) », « countCustomers () »

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Pour répondre à votre question sur les "appels de logique métier", voici un article POSTde Roy Fielding lui-même: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post où l'idée de base est: s'il n'y en a pas 't une méthode (comme GETou PUT) idéalement adaptée à votre utilisation opérationnelle POST.
rojoca

C'est à peu près ce que j'ai fini par faire. Effectuez des appels REST pour récupérer et mettre à jour des ressources connues à l'aide de GET, PUT, DELETE. POST pour l'ajout de nouvelles ressources et POST avec une URL descriptive pour les appels de logique métier.
magiconair

Quoi que vous décidiez, si cette opération ne fait pas partie de la réponse GET, vous n'avez pas de service RESTful. Je ne vois pas ça ici
MStodd

Réponses:


69

Vous avez essentiellement deux options:

  1. Utiliser PATCH(mais notez que vous devez définir votre propre type de média qui spécifie ce qui se passera exactement)

  2. Utilisez POSTune ressource secondaire et renvoyez 303 Voir Autre avec l'en-tête Location pointant vers la ressource principale. L'intention du 303 est de dire au client: "J'ai effectué votre POST et l'effet a été qu'une autre ressource a été mise à jour. Voir l'en-tête Emplacement pour quelle ressource c'était." POST / 303 est destiné aux ajouts itératifs à des ressources pour construire l'état de certaines ressources principales et il convient parfaitement aux mises à jour partielles.


OK, le POST / 303 a du sens pour moi. PATCH et MERGE Je n'ai pas pu trouver dans la liste des verbes HTTP valides, ce qui nécessiterait plus de tests. Comment créer un URI si je veux que le système envoie un e-mail au client 123? Quelque chose comme un appel de méthode RPC pur qui ne change pas du tout l'état de l'objet. Quelle est la manière RESTful de procéder?
magiconair

Je ne comprends pas la question de l'URI de l'e-mail. Voulez-vous implémenter une passerelle que vous pouvez POSTER pour lui envoyer un e-mail ou recherchez-vous mailto: customer.123@service.org?
Jan Algermissen

15
Ni REST ni HTTP n'ont rien à voir avec CRUD à part certaines personnes qui assimilent les méthodes HTTP à CRUD. REST consiste à manipuler l'état des ressources en transférant des représentations. Quoi que vous souhaitiez réaliser, faites-le en transférant une représentation vers une ressource avec la sémantique appropriée. Méfiez-vous des termes «appels de méthode pure» ou «logique métier» car ils impliquent trop facilement «HTTP est pour le transport». Si vous devez envoyer un e-mail, POST à ​​une ressource de passerelle, si vous devez fusionner avec des comptes, créez-en un nouveau et des représentations POST des deux autres, etc.
Jan Algermissen

9
Voir aussi comment Google le fait: 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": "supprimer", "chemin": "/ a / b / c"}, {"op": "ajouter", "chemin": "/ a / b / c" , "valeur": ["foo", "bar"]}, {"op": "replace", "path": "/ a / b / c", "value": 42}, {"op": "move", "from": "/ a / b / c", "path": "/ a / b / d"}, {"op": "copy", "from": "/ a / b / d "," chemin ":" / a / b / e "}]
intotecho

48

Vous devez utiliser POST pour les mises à jour partielles.

Pour mettre à jour les champs du client 123, effectuez un POST vers / client / 123.

Si vous souhaitez mettre à jour uniquement le statut, vous pouvez également METTRE / client / 123 / statut.

Généralement, les requêtes GET ne devraient pas avoir d'effets secondaires, et PUT sert à écrire / remplacer la ressource entière.

Cela découle directement de HTTP, comme on le voit ici: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods


1
@John Saunders POST ne doit pas nécessairement créer une nouvelle ressource accessible à partir d'un URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: Je sais que cela ne doit pas nécessairement aboutir à une nouvelle URL, mais je pensais quand même qu'un POST /customer/123devrait créer la chose évidente qui est logiquement sous le client 123. Peut-être une commande? PUT /customer/123/statussemble avoir un meilleur sens, en supposant que le POST a /customersimplicitement créé un status(et en supposant que c'est REST légitime).
John Saunders

1
@John Saunders: en pratique, si nous voulons mettre à jour un champ sur une ressource située à un URI donné, POST a plus de sens que PUT, et sans UPDATE, je pense qu'il est souvent utilisé dans les services REST. POST vers / clients peut créer un nouveau client, et un statut PUT vers / client / 123 / peut mieux s'aligner avec le mot de la spécification, mais comme pour les meilleures pratiques, je ne pense pas qu'il y ait une raison de ne pas POST vers / client / 123 pour mettre à jour un champ - c'est concis, logique et ne va strictement à l'encontre de rien dans la spécification.
wsorenson

8
Les requêtes POST ne devraient-elles pas être idempotentes? La mise à jour d'une entrée est sûrement idempotente et devrait donc être un PUT à la place?
Martin Andersson

1
@MartinAndersson - les POSTdemandes n'ont pas besoin d'être non idempotentes. Et comme mentionné, PUTdoit remplacer une ressource entière.
Halle Knast

10

Vous devez utiliser PATCH pour les mises à jour partielles - soit en utilisant des documents json-patch (voir http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 ou http://www.mnot.net/ blog / 2012/09/05 / patch ) ou le framework de correctifs XML (voir http://tools.ietf.org/html/rfc5261 ). À mon avis, json-patch est le mieux adapté à votre type de données d'entreprise.

PATCH avec des documents de patch JSON / XML a une sémantique très étroite pour les mises à jour partielles. Si vous commencez à utiliser POST, avec des copies modifiées du document d'origine, pour les mises à jour partielles, vous rencontrez rapidement des problèmes où vous souhaitez que les valeurs manquantes (ou plutôt les valeurs nulles) représentent soit "ignorer cette propriété", soit "définir cette propriété sur le valeur vide "- et cela mène à un trou de lapin de solutions piratées qui se traduira finalement par votre propre type de format de patch.

Vous pouvez trouver une réponse plus approfondie ici: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .


Veuillez noter que, dans l'intervalle, les RFC pour json-patch et xml-patch ont été finalisés.
botchniaque

8

Je rencontre un problème similaire. PUT sur une sous-ressource semble fonctionner lorsque vous souhaitez mettre à jour un seul champ. Cependant, parfois, vous souhaitez mettre à jour un tas de choses: pensez à un formulaire Web représentant la ressource avec l'option de modifier certaines entrées. La soumission du formulaire par l'utilisateur ne doit pas entraîner plusieurs PUT.

Voici deux solutions auxquelles je peux penser:

  1. faire un PUT avec la ressource entière. Côté serveur, définissez la sémantique qu'un PUT avec la ressource entière ignore toutes les valeurs qui n'ont pas changé.

  2. faire un PUT avec une ressource partielle. Côté serveur, définissez la sémantique de ceci comme une fusion.

2 est juste une optimisation de la bande passante de 1. Parfois, 1 est la seule option si la ressource définit certains champs sont des champs obligatoires (pensez proto tampons).

Le problème avec ces deux approches est de savoir comment effacer un champ. Vous devrez définir une valeur nulle spéciale (en particulier pour les tampons de prototypage puisque les valeurs nulles ne sont pas définies pour les tampons de prototypage) qui entraîneront l'effacement du champ.

Commentaires?


2
Ce serait plus utile s'il était affiché comme une question distincte.
intotecho

6

Pour modifier l'état, je pense qu'une approche RESTful consiste à utiliser une sous-ressource logique qui décrit l'état des ressources. Cet IMO est assez utile et propre lorsque vous avez un ensemble réduit de statuts. Il rend votre API plus expressive sans forcer les opérations existantes pour votre ressource client.

Exemple:

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

Le service POST doit renvoyer le client nouvellement créé avec l'ID:

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

Le GET pour la ressource créée utiliserait l'emplacement de la ressource:

GET /customer/123/active

Un GET / client / 123 / inactif doit retourner 404

Pour l'opération PUT, sans fournir d'entité Json, il suffit de mettre à jour l'état

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

Fournir une entité vous permettra de mettre à jour le contenu du client et de mettre à jour le statut en même temps.

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

Vous créez une sous-ressource conceptuelle pour votre ressource client. Elle est également cohérente avec la définition de Roy Fielding d'une ressource: "... Une ressource est un mappage conceptuel avec un ensemble d'entités, pas l'entité qui correspond au mappage à un moment donné dans le temps ..." Dans ce cas, le le mappage conceptuel est actif-client à client avec le statut = ACTIF.

Opération de lecture:

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

Si vous effectuez ces appels l'un après l'autre, l'autre doit renvoyer l'état 404, la sortie réussie peut ne pas inclure l'état car il est implicite. Bien sûr, vous pouvez toujours utiliser GET / customer / 123? Status = ACTIVE | INACTIVE pour interroger directement la ressource client.

L'opération DELETE est intéressante car la sémantique peut être déroutante. Mais vous avez la possibilité de ne pas publier cette opération pour cette ressource conceptuelle, ou de l'utiliser conformément à votre logique métier.

DELETE /customer/123/active

Celui-ci peut amener votre client à un statut SUPPRIMÉ / HANDICAPÉ ou au statut opposé (ACTIF / INACTIF).


Comment accéder à la sous-ressource?
MStodd

J'ai refactorisé la réponse en essayant de la rendre plus claire
raspacorp

5

Choses à ajouter à votre question augmentée. Je pense que vous pouvez souvent parfaitement concevoir des actions commerciales plus compliquées. Mais vous devez abandonner le style de pensée de la méthode / procédure et réfléchir davantage aux ressources et aux verbes.

envois de courrier


POST /customers/123/mails

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

L'implémentation de cette ressource + POST enverrait alors le courrier. si nécessaire, vous pouvez alors proposer quelque chose comme / customer / 123 / outbox, puis proposer des liens de ressources vers / customer / mails / {mailId}.

nombre de clients

Vous pouvez le gérer comme une ressource de recherche (y compris des métadonnées de recherche avec pagination et informations trouvées sur le nombre, ce qui vous donne le nombre de clients).


GET /customers

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


J'aime la façon de regrouper logiquement les champs dans la sous-ressource POST.
gertas

3

Utilisez PUT pour mettre à jour une ressource incomplète / partielle.

Vous pouvez accepter jObject comme paramètre et analyser sa valeur pour mettre à jour la ressource.

Voici la fonction que vous pouvez utiliser comme référence:

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

Concernant votre mise à jour.

Je crois que le concept de CRUD a causé une certaine confusion concernant la conception des API. CRUD est un concept général de bas niveau pour les opérations de base à effectuer sur les données, et les verbes HTTP ne sont que des méthodes de demande ( créé il y a 21 ans ) qui peuvent ou non correspondre à une opération CRUD. En fait, essayez de trouver la présence de l'acronyme CRUD dans la spécification HTTP 1.0 / 1.1.

Un guide très bien expliqué qui applique une convention pragmatique se trouve dans la documentation de l'API Google Cloud Platform . Il décrit les concepts derrière la création d'une API basée sur les ressources, qui met l'accent sur une grande quantité de ressources par rapport aux opérations, et inclut les cas d'utilisation que vous décrivez. Bien que ce soit juste une conception conventionnelle pour leur produit, je pense que cela a beaucoup de sens.

Le concept de base ici (et qui produit beaucoup de confusion) est le mappage entre les "méthodes" et les verbes HTTP. Une chose est de définir quelles "opérations" (méthodes) votre API fera sur quels types de ressources (par exemple, obtenir une liste de clients ou envoyer un e-mail), et une autre sont les verbes HTTP. Il doit y avoir une définition des deux, les méthodes et les verbes que vous prévoyez d'utiliser et un mappage entre eux .

Il dit aussi que, lorsqu'une opération ne correspond pas exactement à une méthode standard ( List, Get,Create , Update, Deletedans ce cas), on peut utiliser des « méthodes personnalisées », comme BatchGet, qui récupère plusieurs objets en fonction de plusieurs entrées identifiant d'objet ou SendEmail.


2

RFC 7396 : JSON Merge Patch (publié quatre ans après la publication de la question) décrit les meilleures pratiques pour un PATCH en termes de format et de règles de traitement.

En bref, vous soumettez un PATCH HTTP à une ressource cible avec le type de média application / merge-patch + json MIME et un corps représentant uniquement les parties que vous souhaitez modifier / ajouter / supprimer, puis suivez les règles de traitement ci-dessous.

Règles :

  • Si le correctif de fusion fourni contient des membres qui n'apparaissent pas dans la cible, ces membres sont ajoutés.

  • Si la cible contient le membre, la valeur est remplacée.

  • Les valeurs nulles dans le correctif de fusion ont une signification spéciale pour indiquer la suppression des valeurs existantes dans la cible.

Exemples de cas de test qui illustrent les règles ci-dessus (comme vu dans l' annexe de ce RFC):

 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

Consultez http://www.odata.org/

Il définit la méthode MERGE, donc dans votre cas, ce serait quelque chose comme ceci:

MERGE /customer/123

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

Seule la statuspropriété est mise à jour et les autres valeurs sont conservées.


Un MERGEverbe HTTP est -il valide?
John Saunders

3
Regardez PATCH - qui sera bientôt le standard HTTP et fait la même chose.
Jan Algermissen

@John Saunders Oui, c'est une méthode d'extension.
Max Toro

FYI MERGE a été supprimé d'OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Voir docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k

0

Ça n'a pas d'importance. En termes de REST, vous ne pouvez pas faire de GET, car il n'est pas mis en cache, mais peu importe si vous utilisez POST ou PATCH ou PUT ou autre chose, et peu importe à quoi ressemble l'URL. Si vous faites REST, ce qui importe, c'est que lorsque vous obtenez une représentation de votre ressource à partir du serveur, cette représentation est en mesure de donner au client des options de transition d'état.

Si votre réponse GET comportait des transitions d'état, le client a juste besoin de savoir comment les lire et le serveur peut les modifier si nécessaire. Ici, une mise à jour est effectuée à l'aide de POST, mais si elle a été modifiée en PATCH, ou si l'URL change, le client sait toujours comment effectuer une mise à jour:

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

Vous pouvez aller jusqu'à lister les paramètres obligatoires / facultatifs que le client vous rendra. Cela dépend de l'application.

En ce qui concerne les opérations commerciales, il peut s'agir d'une ressource différente liée à la ressource client. Si vous souhaitez envoyer un e-mail au client, ce service est peut-être sa propre ressource vers laquelle vous pouvez POSTER, vous pouvez donc inclure l'opération suivante dans la ressource client:

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

Voici de bonnes vidéos et un exemple de l'architecture REST du présentateur. Stormpath utilise uniquement GET / POST / DELETE, ce qui est bien car REST n'a rien à voir avec les opérations que vous utilisez ou l'apparence des URL (sauf que les GET doivent pouvoir être mis en cache):

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.