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 (при условии, что разрешение одобрено). Это подводит меня к первому вопросу:

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

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

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

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

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

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

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

  1. Вы имеете в виду объект, а не коллекцию.

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

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

 { "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 include весь объект, если вы повторяете один и тот же запрос повторно, он всегда должен иметь тот же результат (данные, которые вы отправили, теперь являются целыми данными объекта). Поэтому 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, теперь является адресом электронной почты для объекта.

Итак, когда ПУТЬ не идемпотент, то?

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

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

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

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

На более доступном языке idempotent 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» – это «добавить», поэтому новый пользователь добавляется в список, а последующие 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 на вашем конкретном сервере idempotent. RFC 5789 даже ожидает преимуществ от идемпотентных запросов PATCH:

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

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

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

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 закрытый объект содержит набор инструкций, описывающих, как ресурс, находящийся в настоящее время на исходном сервере, должен быть изменен для создания новой версии. Метод PATCH влияет на ресурс, идентифицированный Request-URI, и также может иметь побочные эффекты для других ресурсов; т.е. новые ресурсы могут быть созданы или изменены существующими путем применения PATCH.

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

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

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

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

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

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

Разница между 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. Если значение поля равно null, это означает удаление этого поля.

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

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

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

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

  • С Spring 3.0, могу ли я сделать необязательную переменную пути?
  • Могу ли я передать загрузку файла на S3 без заголовка содержимого?
  • Что такое программирование RESTful?
  • Как работает аннотация «Spring @ResponseBody» в этом примере приложения RESTful?
  • Как успешно отправить сообщение с помощью нового API GEST REST?
  • В чем разница между HTTP и REST?
  • Сделки в REST?
  • Понимание REST: глаголы, коды ошибок и аутентификация
  • Обнаружение кодировки символов запроса HTTP POST
  • В чем разница между текстом / xml vs application / xml для ответа webservice
  • Когда использовать @QueryParam vs @PathParam
  • Давайте будем гением компьютера.