it-roy-ru.com

REST API - PUT vs PATCH с примерами из реальной жизни

Прежде всего, некоторые определения:

PUT определяется в Раздел 9.6 RFC 2616 :

Метод PUT запрашивает, чтобы вложенный объект был сохранен под предоставленным Request-URI. Если Request-URI ссылается на уже существующий ресурс, вложенный объект СЛЕДУЕТ рассматривать как измененную версию объекта, находящегося на сервере Origin . Если Request-URI не указывает на существующий ресурс и этот URI может быть определен как новый ресурс запрашивающим пользовательским агентом, сервер Origin может создать ресурс с этим URI.

PATCH определен в RFC 5789 :

Метод PATCH запрашивает, чтобы набор изменений , описанных в объекте запроса, был применен к ресурсу, идентифицированному Request-URI.

Также согласно RFC 2616, раздел 9.1.2 PUT является идемпотентным, а PATCH - нет.

Теперь давайте посмотрим на реальный пример. Когда я выполняю POST по /users с данными {username: 'skwee357', email: '[email protected]'} и сервер способен создать ресурс, он отвечает 201 и расположение ресурса (допустим, /users/1), и любой следующий вызов GET /users/1 вернет {id: 1, username: 'skwee357', email: '[email protected]'}.

Теперь допустим, что я хочу изменить свою электронную почту. Модификация электронной почты считается "набором изменений", и поэтому я должен PATCH /users/1 с " patch document ". В моем случае это будет json {email: '[email protected]'}. Затем сервер возвращает 200 (при условии, что разрешение в порядке). Это подводит меня к первому вопросу:

  • Патч не идемпотентен. Об этом говорится в RFC 2616 и RFC 5789. Однако, если я выполню один и тот же запрос PATCH (с моим новым адресом электронной почты), я получу то же состояние ресурса (при изменении моего адреса электронной почты до запрошенного значения). Почему не патч тогда идемпотент?

PATCH - это относительно новый глагол (RFC введен в марте 2010 года), и он решает проблему "исправления" или изменения набора полей. До появления PATCH все использовали PUT для обновления ресурса. Но после того, как был введен PATCH, я не могу понять, для чего тогда используется PUT? И это подводит меня ко второму (и основному) вопросу:

  • В чем реальная разница между PUT и PATCH? Я где-то читал, что PUT может быть использован для замены всей сущности под конкретным ресурсом, поэтому нужно отправить полную сущность (вместо набора атрибутов как с патчем). Каково реальное практическое использование для такого случая? Когда вы хотите заменить/перезаписать объект под конкретным URI ресурса и почему такая операция не рассматривается как обновление/исправление объекта? Единственный практический вариант использования PUT, который я вижу, - это выдача PUT для коллекции, т. Е. /users, чтобы заменить всю коллекцию. Выдача PUT для конкретной сущности не имеет смысла после появления PATCH. Я ошибся?
536
Dmitry Kudryavtsev

NOTE: Когда я впервые потратил время на чтение о REST, идемпотентность была запутанной концепцией, чтобы попытаться получить правильные результаты. Я все еще не совсем понял это в своем первоначальном ответе, как показали дальнейшие комментарии (и ответ Джейсона Хетгера ). Некоторое время я сопротивлялся обновлению этого ответа, чтобы избежать эффективного плагиата Джейсона, но сейчас я его редактирую, потому что, ну, меня попросили (в комментариях).

Прочитав мой ответ, я предлагаю вам также прочитать отличный ответ Джейсона Хетгера на этот вопрос, и я постараюсь сделать свой ответ лучше, не просто крадя у Джейсона.

Почему PUT идемпотент?

Как вы отметили в своей цитате RFC 2616, PUT считается идемпотентным. Когда вы кладете ресурс, в игру вступают два следующих предположения:

  1. Вы имеете в виду сущность, а не коллекцию.

  2. Предоставляемая вами сущность завершена ( вся сущность).

Давайте посмотрим на один из ваших примеров.

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

Если вы POST этот документ в /users, как вы предлагаете, то вы можете получить обратно объект, такой как

## /users/1

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

Если вы хотите изменить эту сущность позже, вы выбираете между PUT и PATCH. PUT может выглядеть так:

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

Вы можете сделать то же самое, используя PATCH. Это может выглядеть так:

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

Вы сразу заметите разницу между этими двумя. PUT включил все параметры этого пользователя, но PATCH включал только тот, который был изменен (email).

При использовании PUT предполагается, что вы отправляете полную сущность, и эта полная сущность заменяет любую существующую сущность по этому URI. В приведенном выше примере PUT и PATCH выполняют одну и ту же цель: они оба меняют адрес электронной почты этого пользователя. Но PUT обрабатывает это, заменяя весь объект, в то время как PATCH обновляет только предоставленные поля, оставляя остальные в покое.

Поскольку запросы PUT включают всю сущность, если вы выполняете один и тот же запрос несколько раз, он всегда должен иметь одинаковый результат (отправленные вами данные теперь являются полными данными сущности). Поэтому PUT идемпотентен.

Неправильное использование PUT

Что произойдет, если вы используете вышеуказанные данные PATCH в запросе 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!
}

(Я предполагаю, что для целей этого вопроса сервер не имеет каких-либо конкретных обязательных полей и позволил бы этому случиться ... что в действительности может быть не так).

Поскольку мы использовали PUT, но предоставили только email, теперь это единственное, что есть в этой сущности. Это привело к потере данных.

Этот пример здесь для иллюстративных целей - никогда не делайте этого на самом деле. Этот запрос PUT технически идемпотентен, но это не значит, что это не ужасная, сломанная идея.

Как PATCH может быть идемпотентом?

В приведенном выше примере PATCH был идемпотентом. Вы внесли изменение, но если вы вносите одно и то же изменение снова и снова, оно всегда будет возвращать один и тот же результат: вы изменили адрес электронной почты на новое значение.

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
}

Мой оригинальный пример, исправленный на точность

Первоначально у меня были примеры, которые, как мне казалось, показывали неидемпотентность, но они вводили в заблуждение/неверно Я собираюсь сохранить примеры, но использую их, чтобы проиллюстрировать другую вещь: то, что несколько документов PATCH против одной и той же сущности, изменяя различные атрибуты, не делают PATCH неидемпотентными.

Допустим, что в свое время пользователь был добавлен. Это состояние, с которого вы начинаете.

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

После патча у вас есть измененный объект:

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"
}

Если вы затем несколько раз примените свой PATCH, вы продолжите получать тот же результат: электронное письмо было изменено на новое значение. А входит, А выходит, поэтому это идемпотент.

Через час, после того как вы пошли приготовить кофе и сделать перерыв, кто-то еще приходит вместе со своим патчем. Кажется, Почта вносит некоторые изменения.

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
}

Так как этот патч из почтового отделения не касается электронной почты, только почтовый индекс, если он применяется повторно, он также получит тот же результат: почтовый индекс установлен на новое значение. А входит, А выходит, поэтому это также идемпотент.

На следующий день вы решили отправить свой патч снова.

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"
}

Ваш патч имеет тот же эффект, что и вчера: он установил адрес электронной почты. А вошел, А вышел, поэтому это тоже идемпотент.

Что я ошибся в своем первоначальном ответе

Я хочу провести важное различие (что-то не так в моем первоначальном ответе). Многие серверы будут отвечать на ваши запросы REST, отправляя обратно новое состояние объекта с вашими изменениями (если таковые имеются). Итак, когда вы получаете этот ответ , он отличается от того, который вы получили вчера , потому что почтовый индекс не тот, который вы получили в прошлый раз. Однако ваш запрос касался не только почтового индекса, но и почтового индекса. Таким образом, ваш документ PATCH все еще идемпотентен - электронное письмо, которое вы отправили в PATCH, теперь является адресом электронной почты объекта.

Так когда же патч не идемпотентен?

Для полного рассмотрения этого вопроса я снова отсылаю вас к ответ Джейсона Хетгера . Я просто собираюсь на этом остановиться, потому что, честно говоря, не думаю, что смогу ответить на этот вопрос лучше, чем он уже сказал.

764
Dan Lowe

Хотя превосходный ответ Дэна Лоу очень подробно ответил на вопрос OP о разнице между PUT и PATCH, его ответ на вопрос, почему PATCH не идемпотентен, не совсем корректен.

Чтобы показать, почему PATCH не идемпотентен, полезно начать с определения идемпотентности (из Википедия ):

Термин идемпотент используется более всесторонне для описания операции, которая будет давать одни и те же результаты при выполнении один или несколько раз [...] Идемпотентная функция - это функция, которая имеет свойство f(f(x)) = f(x) для любого значения x.

На более доступном языке идемпотентный PATCH может быть определен следующим образом: После PATCHing ресурса с документом исправления все последующие вызовы PATCH к одному и тому же ресурсу с тем же документом исправления не изменят ресурс.

И наоборот, неидемпотентная операция - это операция, в которой f(f(x))! = F (x), которая для PATCH может быть обозначена как: после PATCHing ресурса с документом исправления последующие вызовы PATCH к тому же ресурсу с тем же документом исправления измените ресурс .

Чтобы проиллюстрировать неидемпотентный PATCH, предположим, что существует ресурс/users, и предположим, что вызов GET /users возвращает список пользователей, в настоящее время:

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

Вместо того, чтобы PATCHing/users/{id}, как в примере OP, предположим, что сервер разрешает PATCHing/users. Давайте выполним этот запрос PATCH:

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

Наш патч-документ указывает серверу добавить нового пользователя с именем newuser в список пользователей. После первого вызова GET /users вернется:

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

Теперь, если мы выдадим точно такой же запрос PATCH, как описано выше, что произойдет? (Для примера рассмотрим, что ресурс/users допускает дублирование имен пользователей.) "Op" - это "add", поэтому новый пользователь добавляется в список, а последующий GET /users возвращает:

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

Ресурс/users изменился еще раз , хотя мы выдавали точно такой же PATCH против точного та же конечная точка. Если наш PATCH равен f (x), f(f(x)) не совпадает с f (x), и, следовательно, этот конкретный PATCH не идемпотентен .

Хотя PATCH не гарантирует идемпотентности, в спецификации PATCH нет ничего, что могло бы помешать вам выполнять все операции PATCH на вашем конкретном сервере идемпотентами. RFC 5789 даже ожидает преимущества от идемпотентных запросов PATCH:

Запрос PATCH может быть выдан таким образом, чтобы быть идемпотентным, что также помогает предотвратить плохие исходы от коллизий между двумя запросами PATCH на одном и том же ресурсе в аналогичном временном интервале.

В примере Дэна его операция PATCH фактически идемпотентна. В этом примере сущность/users/1 изменилась между нашими запросами PATCH, но не из-за наших запросов PATCH; это был на самом деле другой документ исправления Почты, что привело к изменению почтового индекса. Другой PATCH Почты - это другая операция; если наш PATCH - f (x), то PATCH почтового отделения - g (x). Идемпотентность заявляет, что f(f(f(x))) = f(x), но не дает никаких гарантий относительно f(g(f(x))).

265
Jason Hoetger

Мне тоже было интересно об этом и я нашел несколько интересных статей. Я не могу ответить на ваш вопрос в полной мере, но это, по крайней мере, дает дополнительную информацию.

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

HTTP RFC указывает, что PUT должен принимать полное новое представление ресурса в качестве объекта запроса. Это означает, что если, например, предоставляются только определенные атрибуты, их следует удалить (т. Е. Установить в ноль).

Учитывая это, тогда PUT должен отправить весь объект. Например,

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

Это будет эффективно обновлять электронную почту. Причина, по которой PUT может быть не слишком эффективной, заключается в том, что ваше единственное действительное изменение одного поля, включая имя пользователя, является бесполезным. Следующий пример показывает разницу.

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

Теперь, если PUT был разработан в соответствии со спецификацией, тогда PUT установит имя пользователя в null, и вы получите следующее.

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

Когда вы используете PATCH, вы обновляете только указанное вами поле, а остальные оставляете в покое, как в вашем примере.

Следующий взгляд на патч немного отличается от того, что я никогда раньше не видел.

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

Разница между запросами PUT и PATCH отражается в способе, которым сервер обрабатывает вложенный объект для изменения ресурса, идентифицируемого Request-URI. В запросе PUT вложенный объект считается модифицированной версией ресурса, хранящегося на сервере-источнике, и клиент запрашивает замену сохраненной версии. Однако с помощью PATCH вложенный объект содержит набор инструкций, описывающих, как ресурс, находящийся в данный момент на сервере Origin, должен быть модифицирован для создания новой версии. Метод PATCH влияет на ресурс, идентифицируемый Request-URI, и он также МОЖЕТ иметь побочные эффекты на других ресурсах; то есть новые ресурсы могут быть созданы, или существующие изменены, путем применения PATCH.

PATCH /users/123

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

Вы более или менее рассматриваете PATCH как способ обновления поля. Таким образом, вместо отправки частичного объекта вы отправляете операцию. Т.е. заменить электронную почту значением.

Статья заканчивается этим.

Стоит отметить, что PATCH на самом деле не предназначен для действительно REST API, поскольку в диссертации Филдинга не определен способ частичной модификации ресурсов. Но сам Рой Филдинг сказал, что PATCH был чем-то [он] создан для первоначального предложения HTTP/1.1, потому что частичное PUT никогда не бывает RESTful. Конечно, вы не передаете полное представление, но REST в любом случае не требует, чтобы представления были завершены.

Теперь, я не знаю, согласен ли я с этой статьей, как отмечают многие комментаторы. Отправка по частичному представлению может легко быть описанием изменений.

Для меня я использую PATCH. По большей части я буду относиться к PUT как к PATCH, поскольку единственное реальное отличие, которое я заметил до сих пор, это то, что PUT "должен" установить недостающие значения на ноль. Возможно, это не самый правильный способ сделать это, но удачное кодирование идеально.

70
Kalel Wade

Разница между PUT и PATCH заключается в том, что:

  1. PUT должен быть идемпотентом. Чтобы достичь этого, вы должны поместить весь полный ресурс в тело запроса.
  2. PATCH может быть неидемпотентным. Это подразумевает, что в некоторых случаях это может быть идемпотентом, например, в тех случаях, которые вы описали.

Для PATCH требуется некоторый "язык патчей", чтобы сообщить серверу, как модифицировать ресурс. Вызывающий и сервер должны определить некоторые "операции", такие как "добавить", "заменить", "удалить". Например:

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",
}

Вместо того, чтобы использовать явные поля "операции", язык патчей может сделать его неявным, определив такие соглашения:

в теле запроса PATCH:

  1. Существование поля означает "заменить" или "добавить" это поле.
  2. Если значение поля равно нулю, это означает, что удалить это поле.

С вышеупомянутым соглашением PATCH в примере может принять следующую форму:

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

Который выглядит более кратким и удобным. Но пользователи должны знать о базовом соглашении.

С операциями, которые я упомянул выше, PATCH все еще идемпотентен. Но если вы определите такие операции, как: "increment" или "append", вы можете легко увидеть, что это больше не будет идемпотентом.

12
Bin Ni

Позвольте мне процитировать и прокомментировать более подробно RFC 7231 раздел 4.2.2 , уже упоминавшийся в предыдущих комментариях:

Метод запроса считается "идемпотентным", если предполагаемое воздействие на сервер нескольких идентичных запросов с помощью этого метода такое же, как и эффект для одного такого запроса. Из методов запроса, определенных в этой спецификации, PUT, DELETE и безопасные методы запроса являются идемпотентными.

(...)

Идемпотентные методы отличаются тем, что запрос может повторяться автоматически, если сбой связи происходит до того, как клиент сможет прочитать ответ сервера. Например, если клиент отправляет запрос PUT и базовое соединение закрывается до получения какого-либо ответа, тогда клиент может установить новое соединение и повторить идемпотентный запрос. Он знает, что повторение запроса будет иметь тот же предполагаемый эффект, даже если исходный запрос был выполнен успешно, хотя ответ может отличаться.

Итак, что должно быть "таким же" после повторного запроса идемпотентного метода? Не состояние сервера и не ответ сервера, а предполагаемый эффект . В частности, метод должен быть идемпотентным "с точки зрения клиента". Теперь я думаю, что эта точка зрения показывает, что последний пример в ответ Дэна Лоу , который я не хочу здесь описывать, действительно показывает, что запрос PATCH может быть неидемпотентным (в более естественный способ, чем пример в ответ Джейсона Хетгера ).

Действительно, давайте сделаем пример немного более точным, сделав явное одно намерение для первого клиента. Допустим, этот клиент просматривает список пользователей проекта, чтобы проверить их электронные адреса и почтовые индексы. Он начинает с пользователя 1, замечая, что почтовый индекс верен, а адрес электронной почты неправильный. Он решает исправить это с помощью запроса PATCH, который является полностью законным, и отправляет только

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

так как это единственное исправление. Теперь запрос не выполняется из-за какой-то проблемы в сети и автоматически отправляется через пару часов. Между тем, другой клиент (ошибочно) изменил Zip пользователя 1. Затем, отправка того же запроса PATCH во второй раз не дает предполагаемого эффекта клиента, так как мы в конечном итоге с неправильным Zip. Следовательно, метод не идемпотентен в смысле RFC.

Если вместо этого клиент использует запрос PUT для исправления электронной почты, отправляя на сервер все свойства пользователя 1 вместе с электронной почтой, его ожидаемый эффект будет достигнут, даже если запрос необходимо будет повторно отправить позже, а пользователь 1 был изменен. в то же время --- поскольку второй запрос PUT перезапишет все изменения, начиная с первого запроса.

3
Rolvernew