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

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

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

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

PATCH определяется в RFC 5789:

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

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

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

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

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

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

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

Ответ 1

ПРИМЕЧАНИЕ. Когда я впервые потратил время на чтение REST, идемпотенция была запутанной концепцией, чтобы попытаться получить право. Я до сих пор не понял этого в своем первоначальном ответе, поскольку дальнейшие комментарии (и Jason Hoetger answer) показали. Некоторое время я сопротивлялся обновлению этого ответа, чтобы избежать плагиата Джейсона, но сейчас я его редактирую, потому что, ну, меня просили (в комментариях).

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

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

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

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

  • Объект, который вы поставляете, является полным (весь объект).

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

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

Если вы поместите этот документ в /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 в отношении одного и того же объекта, изменяя разные атрибуты, не делают PATCHes неидентичными.

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

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

После PATCH у вас есть модифицированный объект:

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 из почтового ветки не относится к электронной почте, только почтовый индекс, если он повторно применяется, он также получит тот же результат: почтовый индекс установлен на новое значение. А входит, А выходит, поэтому это тоже идемпотент.

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

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, теперь является адресом электронной почты в сущности.

Итак, когда PATCH не идемпотент, то?

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

Ответ 2

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

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

Термин идемпотент используется более полно для описания операции, которая даст одинаковые результаты, если она будет выполнена один или несколько раз [...] Идемпотентная функция - это функция, которая имеет свойство f (f (x)) = f (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))).

Ответ 3

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

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

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

Учитывая это, 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, вы обновляете только указанное поле и оставляете остальных в покое, как в вашем примере.

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

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

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

PATCH /users/123

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

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

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

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

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

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

Ответ 4

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

  • PUT должен быть идемпотентным. Чтобы достичь этого, вы должны поместить весь полный ресурс в тело запроса.
  • 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:

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

В соответствии с вышеприведенным соглашением, PATCH в примере может иметь следующий вид:

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

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

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

Ответ 5

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

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

(...)

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

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

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

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

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

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

Ответ 6

По моему скромному мнению, идемпотентность означает:

  • PUT:

Итак, я отправляю определение конкурирующего ресурса - итоговое состояние ресурса точно соответствует параметру PUT. Каждый раз, когда я обновляю ресурс теми же параметрами PUT - результирующее состояние точно такое же.

  • PATCH:

Я отправил только часть определения ресурса, так что может случиться так, что другие пользователи в то же время обновляют ДРУГИЕ параметры этого ресурса. Следовательно - последовательные исправления с одинаковыми параметрами и их значениями могут привести к разному состоянию ресурса. Например:

Предположим, что объект определен следующим образом:

АВТОМОБИЛЬ:  - черный цвет,  - тип: седан,  - мест: 5

Я исправляю это с помощью:

{color: 'red'}

Полученный объект:

АВТОМОБИЛЬ:  - красный цвет,  - тип: седан,  - мест: 5

Затем некоторые другие пользователи исправляют этот автомобиль с помощью:

{тип: 'хэтчбек'}

Итак, результирующий объект:

АВТОМОБИЛЬ:  - красный цвет,  - тип: хэтчбек,  - мест: 5

Теперь, если я снова исправлю этот объект с помощью:

{color: 'red'}

результирующий объект:

АВТОМОБИЛЬ:  - красный цвет,  - тип: хэтчбек,  - мест: 5

Чем отличается то, что я получил ранее!

Вот почему PATCH не идемпотентен, а PUT идемпотентен.

Ответ 8

TLDR - упрощенная версия

PUT => Установить все новые атрибуты для существующего ресурса.

PATCH => Частично обновить существующий ресурс (не все атрибуты обязательны).