协慌网

登录 贡献 社区

在 REST API 实际场景中使用 PUT 与 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 是幂等的,而 PATCH 不是。

现在让我们看一个真实的例子。当我使用数据{username: 'skwee357', email: '[email protected]'} /users进行 POST,并且服务器能够创建资源时,它将响应 201 和资源位置(假设/users/1 )和对 GET /users/1下一次调用将返回{id: 1, username: 'skwee357', email: '[email protected]'}

现在,让我们说我想修改我的电子邮件。电子邮件修改被认为是 “一组更改”,因此我应该使用 “补丁文档” 来/users/1在我的情况下,它将是 json 文档: {email: '[email protected]'} 。然后,服务器返回 200(假设允许)。这使我想到第一个问题:

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

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

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

答案

注意:当我第一次花时间阅读有关 REST 的知识时,幂等性是一个难以正确理解的概念。正如我进一步的评论(以及 Jason Hoetger 的答案)所显示的那样,我在原始答案中仍然没有完全正确。一段时间以来,我一直拒绝广泛地更新此答案,以避免有效窃 Jason,但现在我正在对其进行编辑,因为(在评论中)我被要求这样做。

阅读我的回答后,建议您也阅读Jason Hoetger 对这个问题的出色回答,我将尝试使我的回答更好,而不仅仅是从 Jason 那里窃取。

为什么 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 请求包含整个实体,因此,如果您重复发出相同的请求,则它应始终具有相同的结果(您发送的数据现在是该实体的整个数据)。因此,PUT 是幂等的。

错误使用 PUT

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

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

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

一个小时后,您去煮咖啡并休息一会后,其他人便带上了他们自己的 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 本身与电子邮件无关,因此如果重复使用邮政编码,则也将得到相同的结果:邮政编码设置为新值。 A 进去,A 出来,因此这也是幂等的。

第二天,您决定再次发送 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"
}

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

我的原始答案有误

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

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

要对该问题进行全面处理,我再次请您参考Jason Hoetger 的回答。我只是要保留它,因为老实说我认为我不能比他已经更好地回答这部分。

尽管 Dan Lowe 的出色回答非常彻底地回答了 OP 关于 PUT 和 PATCH 之间的区别的问题,但其对为什么 PATCH 不等幂的问题的回答并不十分正确。

为了显示 PATCH 为什么不是幂等的,它有助于从幂等的定义开始(来自Wikipedia ):

幂等一词用于更广泛地描述一种操作,如果执行一次或多次,将产生相同的结果。幂等函数是具有以下特性的函数 f(f(x))= f(x)任何值 x。

用更易访问的语言,幂等的 PATCH 可以定义为:用补丁文档对资源进行补丁之后,对具有相同补丁文档的相同资源的所有后续 PATCH 调用都不会更改该资源。

相反,非等幂运算是其中 f(f(x))!= f(x)的情况,对于 PATCH,可以将其表示为:用补丁文档对资源进行 PATCH 之后,随后的 PATCH 用相同的修补程序文档确实会更改资源。

为了说明一个非幂等的 PATCH,假设有一个 / users 资源,并且假设调用GET /users返回了一个用户列表,当前:

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

假定服务器允许 PATCHing / user,而不是像 OP 的示例中那样 PATCHing / users / {id}。让我们发出以下 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]" }]

即使我们针对完全相同的端点发出了完全相同的PATCH,/ users 资源也再次发生了变化。如果我们的 PATCH 为 f(x),则 f(f(x))与 f(x)不同,因此,该特定 PATCH 不是幂等的

尽管不能保证PATCH 是幂等的,但是 PATCH 规范中没有任何内容可以阻止您对特定服务器进行幂等的所有 PATCH 操作。 RFC 5789 甚至预期幂等 PATCH 请求的优点:

可以以幂等的方式发出 PATCH 请求,这也有助于防止在相同时间段内同一资源上的两个 PATCH 请求之间的冲突导致不良结果。

在 Dan 的示例中,他的 PATCH 操作实际上是幂等的。在该示例中,/ users / 1 实体在我们的 PATCH 请求之间进行了更改,但不是因为我们的 PATCH 请求而发生的;而是实际上是邮局的其他补丁文档导致了邮政编码的更改。邮局的不同 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 并不是真正为真正的 REST API 设计的,因为 Fielding 的论文没有定义任何部分修改资源的方法。但是,罗伊 · 菲尔丁(Roy Fielding)自己说,PATCH 是他为最初的 HTTP / 1.1 提议创建的,因为部分 PUT 从来都不是 RESTful 的。确保您没有传输完整的表示形式,但是 REST 仍然不需要完整的表示形式。

现在,正如许多评论员指出的那样,我不知道我是否特别同意这篇文章。通过部分表示发送可以轻松地描述更改。

对我来说,我对使用 PATCH 感到困惑。在大多数情况下,我会将 PUT 视为 PATCH,因为到目前为止我所注意到的唯一真正的区别是 PUT“应该” 将缺失值设置为 null。这样做可能不是 “最正确” 的方法,但是祝您好运。