Использование методов PUT и PATCH в реальных сценариях API REST

683

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

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 является идемпотентным, а PATCH - нет.

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

Теперь позвольте мне сказать, что я хочу изменить свою электронную почту. Модификация электронной почты считается «набором изменений», и поэтому мне следует патчить /users/1с « патчем документа ». В моем случае это был бы документ в формате JSON: {email: 'skwee357@newdomain.com'}. Затем сервер возвращает 200 (при условии, что разрешение в порядке). Это подводит меня к первому вопросу:

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

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

  • В чем реальная разница между PUT и PATCH? Я где-то читал, что PUT может использоваться для замены всей сущности под конкретным ресурсом, поэтому следует отправить полную сущность (вместо набора атрибутов, как с PATCH). Каково реальное практическое использование для такого случая? Когда вы хотите заменить / перезаписать объект в определенном URI ресурса, и почему такая операция не считается обновлением / исправлением объекта? Единственный практический вариант использования PUT, который я вижу, - это выдача PUT для коллекции, т.е. /usersдля замены всей коллекции. Выдача PUT для конкретной сущности не имеет смысла после появления PATCH. Я ошибаюсь?
Дмитрий Кудрявцев
источник
1
a) это RFC 2616, а не 2612. b) RFC 2616 устарел, текущая спецификация PUT находится в greenbytes.de/tech/webdav/rfc7231.html#PUT , c) я не понимаю ваш вопрос; Разве не очевидно, что PUT можно использовать для замены любого ресурса, а не только коллекции, d) до появления PATCH люди обычно использовали POST, e) наконец, да, определенный запрос PATCH (в зависимости от формата исправления) может быть идемпотентом; просто это не так вообще.
Джулиан Решке
если это поможет, я написал статью о PATCH vs PUT eq8.eu/blogs/36-patch-vs-put-and-the-patch-json-syntax-war
эквивалент
5
Просто: POST создает элемент в коллекции. PUT заменяет элемент. PATCH изменяет элемент. При выполнении POST URL-адрес для нового элемента вычисляется и возвращается в ответе, тогда как PUT и PATCH требуют URL-адреса в запросе. Правильно?
Том Рассел
Этот пост может быть полезен: POST против PUT против PATCH: mscharhag.com/api-design/http-post-put-patch
micha

Ответы:

943

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

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

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

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

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

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

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

{ "username": "skwee357", "email": "skwee357@domain.com" }

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

## /users/1

{
    "username": "skwee357",
    "email": "skwee357@domain.com"
}

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

PUT /users/1
{
    "username": "skwee357",
    "email": "skwee357@gmail.com"       // new email address
}

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

PATCH /users/1
{
    "email": "skwee357@gmail.com"       // new email address
}

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

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

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

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

Что произойдет, если вы используете вышеуказанные данные PATCH в запросе PUT?

GET /users/1
{
    "username": "skwee357",
    "email": "skwee357@domain.com"
}
PUT /users/1
{
    "email": "skwee357@gmail.com"       // new email address
}

GET /users/1
{
    "email": "skwee357@gmail.com"      // new email address... and nothing else!
}

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

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

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

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

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

GET /users/1
{
    "username": "skwee357",
    "email": "skwee357@domain.com"
}
PATCH /users/1
{
    "email": "skwee357@gmail.com"       // new email address
}

GET /users/1
{
    "username": "skwee357",
    "email": "skwee357@gmail.com"       // email address was changed
}
PATCH /users/1
{
    "email": "skwee357@gmail.com"       // new email address... again
}

GET /users/1
{
    "username": "skwee357",
    "email": "skwee357@gmail.com"       // nothing changed since last GET
}

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

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

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

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "skwee357@olddomain.com",
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "zip": "10001"
}

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

PATCH /users/1
{"email": "skwee357@newdomain.com"}

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "skwee357@newdomain.com",    // 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": "skwee357@newdomain.com",  // 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": "skwee357@newdomain.com"}

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "skwee357@newdomain.com",
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "zip": "12345"
}

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

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

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

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

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

Дэн Лоу
источник
2
Это предложение не совсем правильно: «Но оно идемпотентно: всякий раз, когда А входит, В всегда выходит». Например, если GET /users/1до GET /users/1того, как почтовое отделение обновит почтовый индекс, а затем снова сделает тот же запрос после обновления почтового отделения, вы получите два разных ответа (разные почтовые индексы). Идет та же буква «А» (запрос GET), но вы получаете разные результаты. И все же GET все еще идемпотентен.
Джейсон Хетгер
@JasonHoetger GET безопасен (предположительно, не вызывает изменений), но не всегда идемпотентен. Есть разница. См. RFC 2616 сек. 9.1 .
Дэн Лоу
1
@DanLowe: GET определенно гарантированно будет идемпотентом. В нем точно сказано, что в Разделе 9.1.2 RFC 2616 и в обновленной спецификации, Разделе 4.2.2 RFC 7231 , что «Из методов запроса, определенных в этой спецификации, методы PUT, DELETE и safe request являются идемпотентными». Идемпотентность просто не означает «вы получаете один и тот же ответ каждый раз, когда делаете один и тот же запрос». 7231 4.2.2 продолжает говорить: «Повторяя запрос будет иметь тот же ожидаемый эффект, даже если первоначальный запрос успешно, хотя реакция может отличаться. »
Джейсон Hoetger
1
@JasonHoetger Я согласен с этим, но я не понимаю, как это связано с этим ответом, в котором обсуждались PUT и PATCH, и никогда даже не упоминается GET ...
Дэн Лоу
1
Ах, комментарий от @JasonHoetger прояснил это: только результирующие состояния, а не ответы, нескольких запросов идемпотентного метода должны быть идентичны.
Том Рассел
329

Хотя превосходный ответ Дэна Лоу очень подробно ответил на вопрос OP о разнице между 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": "firstuser@example.org" }]

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

PATCH /users
[{ "op": "add", "username": "newuser", "email": "newuser@example.org" }]

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

[{ "id": 1, "username": "firstuser", "email": "firstuser@example.org" },
 { "id": 2, "username": "newuser", "email": "newuser@example.org" }]

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

[{ "id": 1, "username": "firstuser", "email": "firstuser@example.org" },
 { "id": 2, "username": "newuser", "email": "newuser@example.org" },
 { "id": 3, "username": "newuser", "email": "newuser@example.org" }]

Ресурс / 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))).

Джейсон Хетгер
источник
11
Предполагая, что сервер также позволяет выдавать PUT at /users, это также делает PUT неидемпотентным. Все это сводится к тому, как сервер предназначен для обработки запросов.
Узаир Саджид
13
Итак, мы могли бы построить API только с операциями PATCH. Тогда, что становится принципом REST использования http VERBS для выполнения действий CRUD над Ресурсами? Разве мы не слишком усложняем джентльмены границ PATCH здесь?
бор
6
Если PUT реализован в коллекции (например /users), любой запрос PUT должен заменить содержимое этой коллекции. Так что PUT /usersдолжен ожидать коллекцию пользователей и удалить всех остальных. Это идемпотент. Маловероятно, что вы сделали бы такую ​​вещь в конечной точке / users. Но что-то похожее /users/1/emailsможет быть коллекцией, и вполне может быть допустимо заменить всю коллекцию новой.
Vectorjohn
5
Хотя этот ответ является отличным примером идемпотентности, я полагаю, что это может помутить воду в типичных сценариях REST. В этом случае у вас есть запрос PATCH с дополнительным opдействием, которое запускает определенную логику на стороне сервера. Для этого серверу и клиенту необходимо знать конкретные значения, передаваемые opполю для запуска рабочих процессов на стороне сервера. В более простых сценариях REST этот тип opфункциональности является плохой практикой и, вероятно, должен обрабатываться непосредственно через HTTP-глаголы.
Ивандов
7
Я бы никогда не подумал о выдаче PATCH, только POST и DELETE, для коллекции. Это действительно когда-либо было сделано? Может ли PATCH считаться идемпотентом для всех практических целей?
Том Рассел
72

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

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

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

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

/users/1
PUT {id: 1, username: 'skwee357', email: 'newemail@domain.com'}

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

/users/1
PUT {id: 1, email: 'newemail@domain.com'}

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

{id: 1, username: null, email: 'newemail@domain.com'}

Когда вы используете 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": "new.email@example.org" }
]

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

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

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

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

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

Калель Уэйд
источник
7
Возможно, стоит добавить: в статье Уильяма Дюранда (и rfc 6902) есть примеры, где «op» означает «добавить». Это явно не идемпотент.
Йоханнес Бродуолл
2
Или вы можете сделать проще и использовать вместо этого RFC 7396 Merge Patch и избегать сборки патча JSON.
Петр Кула
для таблиц nosql важны различия между patch и put, потому что nosql не имеет столбцов
stackdave
18

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

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

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

GET /contacts/1
{
  "id": 1,
  "name": "Sam Kwee",
  "email": "skwee357@olddomain.com",
  "state": "NY",
  "zip": "10001"
}

PATCH /contacts/1
{
 [{"operation": "add", "field": "address", "value": "123 main street"},
  {"operation": "replace", "field": "email", "value": "abc@myemail.com"},
  {"operation": "delete", "field": "zip"}]
}

GET /contacts/1
{
  "id": 1,
  "name": "Sam Kwee",
  "email": "abc@myemail.com",
  "state": "NY",
  "address": "123 main street",
}

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

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

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

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

PATCH /contacts/1
{
  "address": "123 main street",
  "email": "abc@myemail.com",
  "zip":
}

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

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

Бин Ни
источник
7

TLDR - Dumbed Down Version

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

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

Bijan
источник
3

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

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

(...)

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

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

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

PATCH /users/1
{"email": "skwee357@newdomain.com"}

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

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

Rolvernew
источник
2

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

  • ПОЛОЖИЛ:

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

  • PATCH:

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

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

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

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

{красный цвет'}

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

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

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

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

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

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

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

{красный цвет'}

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

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

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

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

Збигнев Щенсны
источник
1

Чтобы завершить обсуждение идемпотентности, я должен отметить, что идемпотентность в контексте REST можно определить двумя способами. Давайте сначала формализуем несколько вещей:

Ресурс является функцией с его кообластью является классом строк. Другими словами, ресурс - это подмножество String × Any, в котором все ключи уникальны. Давайте назовем класс ресурсов Res.

REST-операция над ресурсами, это функция f(x: Res, y: Res): Res. Два примера операций REST:

  • PUT(x: Res, y: Res): Res = x, а также
  • PATCH(x: Res, y: Res): Res, который работает как PATCH({a: 2}, {a: 1, b: 3}) == {a: 2, b: 3}.

(Это определение специально разработано, чтобы спорить о, PUTи POST, например, не имеет особого смысла GETи POST, поскольку не заботится о постоянстве).

Теперь, исправляя x: Res(информативно говоря, используя карринг), PUT(x: Res)и PATCH(x: Res)получим одномерные функции типа Res → Res.

  1. Функция g: Res → Resназывается глобально идемпотентна , когда g ○ g == g, то есть для любого y: Res, g(g(y)) = g(y).

  2. Пусть x: Resресурс, а k = x.keys. Функция g = f(x)называется левым идемпотентом , когда для каждого y: Resмы имеем g(g(y))|ₖ == g(y)|ₖ. Это в основном означает, что результат должен быть таким же, если мы посмотрим на примененные ключи.

Таким образом, PATCH(x)не является идемпотентом в глобальном масштабе, но остается идемпотентом. И здесь важна левая идемпотентность: если мы исправляем несколько ключей ресурса, мы хотим, чтобы эти ключи были такими же, если мы исправляем его снова, и нам нет дела до остальной части ресурса.

И когда RFC говорит о том, что PATCH не идемпотентен, он говорит о глобальной идемпотентности. Хорошо, что это не глобально идемпотент, иначе это была бы сломанная операция.


Теперь, ответ Джейсона Хетгера пытается продемонстрировать, что PATCH даже не остается идемпотентным, но он ломает слишком много вещей, чтобы сделать это:

  • Прежде всего, PATCH используется в наборе, хотя PATCH определен для работы с картами / словарями / объектами значения ключа.
  • Если кто-то действительно хочет применить PATCH к наборам, то следует использовать естественный перевод:, t: Set<T> → Map<T, Boolean>определенный с помощью x in A iff t(A)(x) == True. Используя это определение, патчирование остается идемпотентным.
  • В этом примере этот перевод не использовался, вместо этого PATCH работает как POST. Прежде всего, почему для объекта генерируется идентификатор? И когда это генерируется? Если объект сначала сравнивается с элементами набора, и если соответствующий объект не найден, то генерируется идентификатор, затем снова программа должна работать по-другому ( {id: 1, email: "me@site.com"}должна совпадать с {email: "me@site.com"}, в противном случае программа всегда не работает, и PATCH не может быть возможно патч). Если идентификатор сгенерирован перед проверкой на соответствие, снова программа не работает.

Можно привести примеры того, что PUT неидемпотентен, если сломать половину того, что сломано в этом примере:

  • Примером сгенерированных дополнительных функций будет управление версиями. Можно вести учет количества изменений на одном объекте. В этом случае PUT не идемпотентен: PUT /user/12 {email: "me@site.com"}результаты в {email: "...", version: 1}первый раз и {email: "...", version: 2}во второй раз.
  • Взаимодействуя с идентификаторами, каждый раз, когда объект обновляется, можно генерировать новый идентификатор, что приводит к неидемпотентному PUT.

Все приведенные выше примеры являются естественными примерами, с которыми можно столкнуться.


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

Мухаммед Али А'Раби
источник
-1

Одна дополнительная информация, которую я только хочу добавить, состоит в том, что запрос PATCH использует меньшую полосу пропускания по сравнению с запросом PUT, поскольку отправляется только часть данных, а не весь объект. Поэтому просто используйте запрос PATCH для обновления определенных записей, таких как (1-3 записи), а запрос PUT для обновления большего объема данных. Вот и все, не думай слишком много и не беспокойся об этом слишком много.

Вениамин
источник