小编典典

在 REST API 现实生活场景中使用 PUT 与 PATCH 方法

all

首先,一些定义:

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 不是。

现在让我们看一个真实的例子。当我/users对数据进行 POST{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(假设权限正常)。这让我想到了第一个问题:

  • PATCH 不是幂等的。它在 RFC 2616 和 RFC 5789 中这样说。但是,如果我发出相同的 PATCH 请求(使用我的新电子邮件),我将获得相同的资源状态(我的电子邮件被修改为请求的值)。为什么 PATCH 不是幂等的?

PATCH 是一个相对较新的动词(RFC 于 2010 年 3 月推出),用于解决“打补丁”或修改一组字段的问题。在引入 PATCH 之前,大家都使用
PUT 来更新资源。但是在引入 PATCH 之后,它让我对 PUT 的用途感到困惑。这让我想到了我的第二个(也是主要的)问题:

  • PUT 和 PATCH 之间的真正区别是什么?我在某处读到 PUT 可能用于 替换 特定资源下的整个实体,因此应该发送完整的实体(而不是像 PATCH 那样的属性集)。这种情况的真正实际用途是什么?您何时想替换/覆盖特定资源 URI 处的实体,为什么不考虑更新/修补实体这样的操作?我看到的 PUT 的唯一实际用例是在集合上发出 PUT,即/users替换整个集合。引入 PATCH 后,在特定实体上发出 PUT 毫无意义。我错了吗?

阅读 224

收藏
2022-02-28

共1个答案

小编典典

注意 :当我第一次花时间阅读 REST 时,幂等性是一个难以理解的概念。正如进一步的评论(和Jason Hoetger
的回答
)所显示的那样,我的原始答案仍然没有完全正确。有一段时间,我一直拒绝广泛更新这个答案,以避免有效地抄袭杰森,但我现在正在编辑它,因为,好吧,我被要求(在评论中)。

看完我的回答后,我建议你也阅读Jason
Hoetger
对这个问题的出色回答,我会尽量让我的回答变得更好,而不是简单地从
Jason 那里偷东西。

为什么 PUT 是幂等的?

正如您在 RFC 2616 引用中所指出的,PUT 被认为是幂等的。当你 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 错误

如果在 PUT 请求中使用上述 PATCH 数据会发生什么?

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 之后,您有一个修改过的实体:

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,您将继续得到相同的结果:电子邮件已更改为新值。A进去,A出来,所以这是幂等的。

一个小时后,在你去泡咖啡休息一下后,其他人带着他们自己的 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 不关心电子邮件,只关心邮政编码,如果重复应用,也会得到相同的结果:邮政编码设置为新值。A进去,A出来,所以这 也是 幂等的。

第二天,您决定再次发送您的 PATCH。

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

您的补丁与昨天的效果相同:它设置了电子邮件地址。A进去,A出来,所以这也是幂等的。

我原来的答案有什么问题

我想画一个重要的区别(我在原来的答案中弄错了)。许多服务器将通过发回新实体状态以及您的修改(如果有)来响应您的 REST 请求。因此,当您收到此 回复
时,它与 您昨天收到的回复 不同,因为邮政编码不是您上次收到的。但是,您的请求与邮政编码无关,仅与电子邮件有关。所以你的 PATCH
文档仍然是幂等的——你在 PATCH 中发送的电子邮件现在是实体上的电子邮件地址。

那么什么时候 PATCH 不是幂等的呢?

对于这个问题的完整处理,我再次向您推荐 Jason Hoetger
的回答
。我只想把它留在那里,因为老实说,我认为我不能比他已经回答的更好。

2022-02-28