web-dev-qa-db-fra.com

REST API - PUT vs PATCH avec des exemples concrets

Tout d’abord, quelques définitions:

PUT est défini dans Section 9.6 RFC 2616 :

La méthode PUT demande à ce que l'entité incluse soit stockée sous l'URI de demande fourni. Si l'URI de demande fait référence à une ressource déjà existante, l'entité incluse DEVRAIT être considérée comme une version modifiée de celle résidant sur le serveur d'origine . Si Request-URI ne pointe pas vers une ressource existante et que cet URI peut être défini en tant que nouvelle ressource par l'agent utilisateur demandeur, le serveur d'origine peut créer la ressource avec cet URI.

PATCH est défini dans RFC 5789 :

La méthode PATCH demande à ce que un ensemble de modifications décrites dans l'entité de demande soit appliqué à la ressource identifiée par l'URI de demande.

Toujours selon RFC 2616, section 9.1.2 , PUT est idempotent alors que PATCH ne l’est pas.

Voyons maintenant un exemple réel. Quand je fais POST sur /users avec les données {username: 'skwee357', email: '[email protected]'} et que le serveur est capable de créer une ressource, il répondra par 201 et son emplacement (supposons que /users/1 ) et tout appel suivant à GET /users/1 renverra {id: 1, username: 'skwee357', email: '[email protected]'}.

Maintenant, disons que je veux modifier mon email. La modification de courrier électronique est considérée comme "un ensemble de modifications" et c'est pourquoi je devrais appliquer PATCH /users/1 avec " document correctif ". Dans mon cas, ce serait un json {email: '[email protected]'}. Le serveur renvoie ensuite 200 (en supposant que les autorisations sont ok). Cela m'amène à la première question:

  • PATCH n'est PAS idempotent. C'est ce qui a été dit dans les documents RFC 2616 et 5789. Toutefois, si j'émettrais la même demande PATCH (avec mon nouveau courrier électronique), j'obtiendrais le même état de ressource (mon courrier électronique étant modifié pour correspondre à la valeur demandée). Pourquoi pas PATCH alors idempotent?

PATCH est un verbe relativement nouveau (RFC introduit en mars 2010), et il vient résoudre le problème du "patch" ou de la modification d'un ensemble de champs. Avant que PATCH soit introduit, tout le monde utilisait PUT pour mettre à jour la ressource. Mais après l’introduction de PATCH, cela me laisse perplexe à quoi sert PUT alors? Et cela m'amène à la deuxième (et principale) question:

  • Quelle est la vraie différence entre PUT et PATCH? J'ai lu quelque part le PUT pourrait être utilisé pour remplacer l'entité entière par une ressource spécifique, il est donc nécessaire d'envoyer l'entité complète (au lieu d'un ensemble d'attributs). avec PATCH). Quel est le véritable usage pratique dans un tel cas? Quand souhaitez-vous remplacer/écraser une entité sous une adresse URI de ressource spécifique et pourquoi une telle opération n'est-elle pas considérée comme une mise à jour/une correction de l'entité? Le seul cas d'utilisation pratique que je connaisse pour PUT est la publication d'un PUT sur la collection, c'est-à-dire /users pour remplacer la collection entière. L'émission de PUT sur une entité spécifique n'a aucun sens après l'introduction de PATCH. Ai-je tort?
569
Dmitry Kudryavtsev

NOTE: Lorsque j'ai commencé à lire pour la première fois à propos de REST, idempotence était un concept déroutant pour essayer de bien faire les choses. Je ne l'ai toujours pas bien compris dans ma réponse initiale, comme l'ont montré d'autres commentaires (et réponse de Jason Hoetger ). Pendant un certain temps, j'ai longtemps refusé de mettre à jour cette réponse afin d'éviter de plagier efficacement Jason, mais je la modifie maintenant car, eh bien, on m'a demandé de le faire (dans les commentaires).

Après avoir lu ma réponse, je vous suggère également de lire excellente réponse de Jason Hoetger à cette question, et je vais essayer d’améliorer ma réponse sans voler simplement Jason.

Pourquoi PUT est-il idempotent?

Comme vous l'avez noté dans votre citation RFC 2616, PUT est considéré comme idempotent. Lorsque vous mettez une ressource, ces deux hypothèses sont en jeu:

  1. Vous faites référence à une entité, pas à une collection.

  2. L'entité que vous fournissez est complète (l'entité entière ).

Regardons l'un de vos exemples.

{ "username": "skwee357", "email": "[email protected]" }

Si vous POST ce document à /users, comme vous le suggérez, alors vous pourriez récupérer une entité telle que

## /users/1

{
    "username": "skwee357",
    "email": "[email protected]"
}

Si vous souhaitez modifier cette entité ultérieurement, vous choisissez entre PUT et PATCH. Un PUT pourrait ressembler à ceci:

PUT /users/1
{
    "username": "skwee357",
    "email": "[email protected]"       // new email address
}

Vous pouvez accomplir la même chose en utilisant PATCH. Cela pourrait ressembler à ceci:

PATCH /users/1
{
    "email": "[email protected]"       // new email address
}

Vous remarquerez tout de suite une différence entre ces deux-là. Le PUT incluait tous les paramètres de cet utilisateur, mais PATCH n'incluait que celui en cours de modification (email).

Lorsque vous utilisez PUT, il est supposé que vous envoyez l'entité complète et que l'entité complète remplace toute entité existante à cet URI. Dans l'exemple ci-dessus, PUT et PATCH remplissent le même objectif: ils changent tous les deux l'adresse électronique de cet utilisateur. Mais PUT le gère en remplaçant l'entité entière, tandis que PATCH ne met à jour que les champs fournis, laissant les autres seuls.

Étant donné que les demandes PUT incluent l'entité entière, si vous émettez la même demande de manière répétée, elle devrait toujours avoir le même résultat (les données que vous avez envoyées sont désormais les données entières de l'entité). Par conséquent, PUT est idempotent.

Utilisation incorrecte de PUT

Que se passe-t-il si vous utilisez les données PATCH ci-dessus dans une requête PUT?

GET /users/1
{
    "username": "skwee357",
    "email": "[email protected]"
}
PUT /users/1
{
    "email": "[email protected]"       // new email address
}

GET /users/1
{
    "email": "[email protected]"      // new email address... and nothing else!
}

(Je suppose que, pour les besoins de cette question, le serveur ne comporte pas de champs obligatoires spécifiques et permettrait que cela se produise ... ce n'est peut-être pas le cas en réalité.)

Puisque nous avons utilisé PUT, mais uniquement fourni email, c'est maintenant la seule chose dans cette entité. Cela a entraîné une perte de données.

Cet exemple est présenté à des fins d'illustration - ne le faites jamais réellement. Cette requête PUT est techniquement idempotente, mais cela ne veut pas dire que ce n’est pas une idée brisée, terrible.

Comment PATCH peut-il être idempotent?

Dans l'exemple ci-dessus, PATCH était idempotent. Vous avez apporté une modification, mais si vous apportiez la même modification encore et encore, le résultat serait toujours identique: vous avez modifié l'adresse électronique en fonction de la nouvelle valeur.

GET /users/1
{
    "username": "skwee357",
    "email": "[email protected]"
}
PATCH /users/1
{
    "email": "[email protected]"       // new email address
}

GET /users/1
{
    "username": "skwee357",
    "email": "[email protected]"       // email address was changed
}
PATCH /users/1
{
    "email": "[email protected]"       // new email address... again
}

GET /users/1
{
    "username": "skwee357",
    "email": "[email protected]"       // nothing changed since last GET
}

Mon exemple original, corrigé pour plus de précision

À l'origine, j'avais des exemples qui, à mon avis, montraient la non-idempotence, mais ils étaient trompeurs/incorrects. Je vais garder les exemples, mais les utiliser pour illustrer une chose différente: que plusieurs documents PATCH contre la même entité, en modifiant des attributs différents, ne rendent pas les PATCH non idempotents.

Disons que par le passé, un utilisateur a été ajouté. C'est l'état à partir duquel vous partez.

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "Zip": "10001"
}

Après un PATCH, vous avez une entité modifiée:

PATCH /users/1
{"email": "[email protected]"}

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",    // the email changed, yay!
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "Zip": "10001"
}

Si vous appliquez ensuite votre PATCH de manière répétée, vous continuerez à obtenir le même résultat: le courrier électronique a été remplacé par la nouvelle valeur. A entre, A sort, c’est donc idempotent.

Une heure plus tard, après que vous soyez allé prendre un café et prendre une pause, quelqu'un d'autre vient avec son propre PATCH. Il semble que le bureau de poste ait apporté des changements.

PATCH /users/1
{"Zip": "12345"}

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",  // still the new email you set
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "Zip": "12345"                      // and this change as well
}

Comme ce PATCH de la poste ne concerne pas la messagerie électronique, seul le code postal, s'il est appliqué de manière répétée, donnera également le même résultat: le code postal est défini sur la nouvelle valeur. A entre, A sort, c’est donc aussi idempotent.

Le lendemain, vous décidez d'envoyer votre PATCH à nouveau.

PATCH /users/1
{"email": "[email protected]"}

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "Zip": "12345"
}

Votre patch a le même effet qu’hier: il a défini l’adresse e-mail. A est entré, A est sorti, donc c'est idempotent aussi.

Ce que je me suis trompé dans ma réponse originale

Je veux faire une distinction importante (quelque chose que je me suis trompé dans ma réponse originale). De nombreux serveurs répondront à vos demandes REST en renvoyant le nouvel état de l'entité, avec vos modifications (le cas échéant). Ainsi, lorsque vous obtenez cette réponse en retour, elle est différente de celle que vous avez obtenue hier , car le code postal n'est pas celui que vous avez reçu la dernière fois. Cependant, votre demande ne concernait pas le code postal, mais uniquement le courrier électronique. Votre document PATCH est donc toujours idempotent - l’email que vous avez envoyé dans PATCH est maintenant l’adresse email de l’entité.

Alors, quand est-ce que PATCH n'est pas idempotent?

Pour un traitement complet de cette question, je vous renvoie à nouveau à réponse de Jason Hoetger . Je vais simplement en rester là, parce que honnêtement, je ne pense pas pouvoir répondre à cette partie mieux qu'il ne l'a déjà fait.

808
Dan Lowe

Bien que l'excellente réponse de Dan Lowe réponde de manière très détaillée à la question du PO concernant la différence entre PUT et PATCH, sa réponse à la question de savoir pourquoi PATCH n'est pas idempotent n'est pas tout à fait correcte.

Pour montrer pourquoi PATCH n'est pas idempotent, il est utile de commencer par la définition de idempotence (de Wikipedia ):

Le terme idempotent est utilisé de manière plus détaillée pour décrire une opération qui produira les mêmes résultats si elle est exécutée une ou plusieurs fois [...] Une fonction idempotente est une fonction qui a la propriété f(f(x)) = f(x) pour toute valeur x.

Dans un langage plus accessible, un PATCH idempotent pourrait être défini ainsi: Après PATCH sur une ressource avec un document de correctif, tous les appels PATCH ultérieurs à la même ressource avec le même document de correctif ne changeront pas la ressource.

Inversement, une opération non idempotente est une opération où f(f(x))! = F (x), ce qui pour PATCH pourrait être défini comme suit: Après PATCHing une ressource avec un document correctif, les appels PATCH suivants sur la même ressource avec le même document correctif changez la ressource .

Pour illustrer un PATCH non idempotent, supposons qu'il existe une ressource/users et supposons que l'appel de GET /users renvoie une liste d'utilisateurs, actuellement:

[{ "id": 1, "username": "firstuser", "email": "[email protected]" }]

Plutôt que PATCHing/users/{id}, comme dans l'exemple du PO, supposons que le serveur autorise PATCHing/users. Émettons cette requête PATCH:

PATCH /users
[{ "op": "add", "username": "newuser", "email": "[email protected]" }]

Notre document de correctif indique au serveur d'ajouter un nouvel utilisateur appelé newuser à la liste des utilisateurs. Après avoir appelé ceci pour la première fois, GET /users renverrait:

[{ "id": 1, "username": "firstuser", "email": "[email protected]" },
 { "id": 2, "username": "newuser", "email": "[email protected]" }]

Maintenant, si nous émettons la demande exacte même PATCH comme ci-dessus, que se passe-t-il? (Pour les besoins de cet exemple, supposons que la ressource/users autorise les noms d'utilisateur en double.) Le "op" est "add", de sorte qu'un nouvel utilisateur est ajouté à la liste et qu'un GET /users suivant retourne:

[{ "id": 1, "username": "firstuser", "email": "[email protected]" },
 { "id": 2, "username": "newuser", "email": "[email protected]" },
 { "id": 3, "username": "newuser", "email": "[email protected]" }]

La ressource/users a changé à nouveau , même si nous avons émis le même exact PATCH contre le exact même point final. Si notre PATCH est f (x), f(f(x)) n'est pas identique à f (x), et par conséquent, , ce PATCH n'est pas idempotent. .

Bien que PATCH ne soit pas garanti d'être idempotent, rien dans la spécification PATCH ne vous empêche de rendre toutes les opérations PATCH idempotentes sur votre serveur. La RFC 5789 anticipe même les avantages des requêtes idempotentes PATCH:

Une demande PATCH peut être émise de manière à être idempotente, ce qui permet également d'éviter de mauvais résultats des collisions entre deux demandes PATCH sur la même ressource dans un laps de temps similaire.

Dans l'exemple de Dan, son opération PATCH est en fait idempotente. Dans cet exemple, l'entité/users/1 a changé entre nos requêtes PATCH, mais pas à cause de nos requêtes PATCH; c'est en fait le document correctif différent de la Poste qui a entraîné la modification du code postal. Le PATCH différent de la Poste est une opération différente. si notre PATCH est f (x), le PATCH de la poste est g (x). Idempotence déclare que f(f(f(x))) = f(x), mais ne donne aucune garantie sur f(g(f(x))).

281
Jason Hoetger

J'étais curieux à ce sujet aussi et j'ai trouvé quelques articles intéressants. Je ne réponds peut-être pas à votre question dans toute son étendue, mais ceci fournit au moins quelques informations supplémentaires.

http://restful-api-design.readthedocs.org/en/latest/methods.html

La RFC HTTP spécifie que PUT doit prendre une nouvelle représentation de ressource complète en tant qu'entité de demande. Cela signifie que si, par exemple, certains attributs seulement sont fournis, ceux-ci doivent être supprimés (c'est-à-dire définis sur null).

Compte tenu de cela, alors un PUT devrait envoyer l'objet entier. Par exemple,

/users/1
PUT {id: 1, username: 'skwee357', email: '[email protected]'}

Cela mettrait effectivement à jour le courrier électronique. La raison pour laquelle PUT n'est peut-être pas aussi efficace est que vous ne pouvez réellement modifier qu'un seul champ et inclure le nom d'utilisateur. L'exemple suivant montre la différence.

/users/1
PUT {id: 1, email: '[email protected]'}

Maintenant, si le PUT a été conçu selon les spécifications, il définira alors le nom d'utilisateur sur null et vous obtiendrez le résultat suivant.

{id: 1, username: null, email: '[email protected]'}

Lorsque vous utilisez un PATCH, vous ne mettez à jour que le champ que vous spécifiez et laissez le reste comme dans votre exemple.

La version suivante du PATCH est un peu différente de celle que je n’ai jamais vue auparavant.

http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/

La différence entre les requêtes PUT et PATCH est reflétée dans la façon dont le serveur traite l'entité incluse pour modifier la ressource identifiée par l'URI de demande. Dans une demande PUT, l'entité incluse est considérée comme une version modifiée de la ressource stockée sur le serveur Origin et le client demande le remplacement de la version stockée. Cependant, avec PATCH, l'entité incluse contient un ensemble d'instructions décrivant la manière dont une ressource résidant actuellement sur le serveur Origin doit être modifiée pour produire une nouvelle version. La méthode PATCH affecte la ressource identifiée par l'URI de demande, et PEUT aussi avoir des effets secondaires sur d'autres ressources; c'est-à-dire que de nouvelles ressources peuvent être créées ou que des ressources existantes peuvent être modifiées par l'application d'un PATCH.

PATCH /users/123

[
    { "op": "replace", "path": "/email", "value": "[email protected]" }
]

Vous traitez plus ou moins le PATCH comme un moyen de mettre à jour un champ. Ainsi, au lieu d'envoyer l'objet partiel, vous envoyez l'opération. c'est-à-dire remplacer l'email par la valeur.

L'article se termine par ceci.

Il est à noter que PATCH n'est pas vraiment conçu pour les API réellement REST, car la thèse de Fielding ne définit aucun moyen de modifier partiellement les ressources. Mais Roy Fielding a lui-même déclaré que PATCH avait été créé par lui pour la proposition initiale HTTP/1.1, car PUT partiel n’était jamais RESTful. Bien sûr, vous ne transférez pas une représentation complète, mais REST n'exige pas que les représentations soient complètes de toute façon.

Maintenant, je ne sais pas si je suis particulièrement d'accord avec l'article, comme le soulignent de nombreux commentateurs. Envoyer sur une représentation partielle peut facilement être une description des modifications.

Pour moi, je suis mélangé sur l'utilisation de PATCH. Pour la plupart, je traiterai PUT comme un PATCH car la seule différence réelle que j'ai remarquée jusqu'à présent est que PUT "devrait" définir les valeurs manquantes sur null. Ce n'est peut-être pas la façon la plus "correcte" de le faire, mais bonne chance, coder parfaitement.

70
Kalel Wade

La différence entre PUT et PATCH est que:

  1. PUT doit être idempotent. Pour ce faire, vous devez placer la totalité de la ressource complète dans le corps de la demande.
  2. PATCH peut être non-idempotent. Ce qui implique que cela peut aussi être idempotent dans certains cas, tels que les cas que vous avez décrits.

PATCH nécessite un "langage de correctif" pour indiquer au serveur comment modifier la ressource. L'appelant et le serveur doivent définir certaines "opérations" telles que "ajouter", "remplacer", "supprimer". Par exemple:

GET /contacts/1
{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",
  "state": "NY",
  "Zip": "10001"
}

PATCH /contacts/1
{
 [{"operation": "add", "field": "address", "value": "123 main street"},
  {"operation": "replace", "field": "email", "value": "[email protected]"},
  {"operation": "delete", "field": "Zip"}]
}

GET /contacts/1
{
  "id": 1,
  "name": "Sam Kwee",
  "email": "[email protected]",
  "state": "NY",
  "address": "123 main street",
}

Au lieu d'utiliser des champs "d'opération" explicites, le langage de correctif peut le rendre implicite en définissant des conventions telles que:

dans le corps de la demande PATCH:

  1. L'existence d'un champ signifie "remplacer" ou "ajouter" ce champ.
  2. Si la valeur d'un champ est null, cela signifie supprimer ce champ.

Avec la convention ci-dessus, le PATCH dans l'exemple peut prendre la forme suivante:

PATCH /contacts/1
{
  "address": "123 main street",
  "email": "[email protected]",
  "Zip":
}

Ce qui semble plus concis et convivial. Mais les utilisateurs doivent être conscients de la convention sous-jacente.

Avec les opérations que j'ai mentionnées ci-dessus, le PATCH est toujours idempotent. Mais si vous définissez des opérations comme: "incrémenter" ou "ajouter", vous pouvez facilement voir que cela ne sera plus idempotent.

13
Bin Ni

Permettez-moi de citer et de commenter plus précisément le RFC 7231 section 4.2.2 , déjà cité dans les commentaires précédents:

Une méthode de requête est considérée comme "idempotente" si l’effet recherché sur le serveur de plusieurs requêtes identiques avec cette méthode est identique à l’effet d’une seule requête de ce type. Parmi les méthodes de requête définies par cette spécification, les méthodes de requête PUT, DELETE et safe sont idempotentes.

(...)

Les méthodes idempotentes sont distinguées car la demande peut être répétée automatiquement en cas d'échec de la communication avant que le client ne puisse lire la réponse du serveur. Par exemple, si un client envoie une demande PUT et que la connexion sous-jacente est fermée avant toute réponse, le client peut établir une nouvelle connexion et réessayer la demande idempotent. Il sait que la répétition de la demande aura le même effet souhaité, même si la demande initiale aboutissait, bien que la réponse puisse être différente.

Alors, que devrait être "le même" après une demande répétée d'une méthode idempotente? Pas l'état du serveur, ni la réponse du serveur, mais l'effet recherché . En particulier, la méthode devrait être idempotente "du point de vue du client". Maintenant, je pense que ce point de vue montre que le dernier exemple de réponse de Dan Lowe , que je ne veux pas plagier ici, montre bien qu'une requête PATCH peut être non idempotente (dans un manière plus naturelle que l'exemple de réponse de Jason Hoetger ).

En effet, rendons l’exemple un peu plus précis en rendant explicite un possible intentionnel pour le premier client. Supposons que ce client parcourt la liste des utilisateurs du projet pour vérifier leurs emails et leurs codes postaux . Il commence par l'utilisateur 1, remarque que le fichier Zip est correct mais que le courrier électronique est incorrect. Il décide de corriger cela avec une requête PATCH, qui est tout à fait légitime, et envoie seulement

PATCH /users/1
{"email": "[email protected]"}

puisque c'est la seule correction. Désormais, la demande échoue à cause d'un problème de réseau et est soumise à nouveau automatiquement quelques heures plus tard. Dans l’intervalle, un autre client a modifié (à tort) le zip de l’utilisateur 1. Ensuite, l’envoi de la même requête PATCH une seconde fois ne produit pas l’effet souhaité du client, car nous nous retrouvons avec un zip incorrect. Par conséquent, la méthode n'est pas idempotente au sens de la RFC.

Si, au lieu de cela, le client utilise une requête PUT pour corriger le courrier électronique, en envoyant au serveur toutes les propriétés de l'utilisateur 1 en même temps que le courrier électronique, l'effet escompté est obtenu même si la demande doit être renvoyée ultérieurement et que l'utilisateur 1 a été modifié. en attendant --- puisque la deuxième demande PUT écrasera toutes les modifications depuis la première demande.

3
Rolvernew
0
jobima