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

Posted

技术标签:

【中文标题】在 REST API 现实生活场景中使用 PUT 与 PATCH 方法【英文标题】:Use of PUT vs PATCH methods in REST API real life scenarios 【发布时间】:2015-04-12 02:57:20 【问题描述】:

首先,一些定义:

PUT 定义在Section 9.6 RFC 2616:

PUT 方法请求将封闭的实体存储在提供的 Request-URI 下。如果 Request-URI 引用了一个已经存在的资源,则封闭的实体应该被视为驻留在源服务器上的资源的修改版本。如果 Request-URI 不指向现有资源,并且该 URI 能够被请求用户代理定义为新资源,则源服务器可以使用该 URI 创建资源。

PATCH 定义在RFC 5789:

PATCH 方法要求一组更改在 请求实体应用于请求标识的资源- URI。

同样根据RFC 2616 Section 9.1.2 PUT 是幂等的,而 PATCH 不是。

现在让我们看一个真实的例子。当我使用数据username: 'skwee357', email: 'skwee357@domain.com' POST 到/users 并且服务器能够创建资源时,它将以201 和资源位置响应(假设/users/1)并且任何下一次对GET /users/1 的调用都将返回id: 1, username: 'skwee357', email: 'skwee357@domain.com'.

现在让我们说我想修改我的电子邮件。电子邮件修改被认为是“一组更改”,因此我应该使用“patch document”修补/users/1。在我的情况下,它将是 json 文档:email: 'skwee357@newdomain.com'。然后服务器返回 200(假设权限正常)。这让我想到了第一个问题:

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

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

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

【问题讨论】:

a) 它是 RFC 2616,而不是 2612。b) RFC 2616 已经过时,PUT 的当前规范在 greenbytes.de/tech/webdav/rfc7231.html#PUT,c) 我不明白你的问题;不是很明显 PUT 可以用来替换任何资源,不仅是一个集合,d) 在引入 PATCH 之前,人们通常使用 POST,e) 最后,是的,一个 specific PATCH 请求(取决于补丁格式)可以是幂等的;只是不是一般的。 如果有帮助,我写了一篇关于 PATCH 与 PUT 的文章 eq8.eu/blogs/36-patch-vs-put-and-the-patch-json-syntax-war 简单:POST 在集合中创建一个项目。 PUT 替换一个项目。 PATCH 修改一个项目。 POST 时,计算新项目的 URL 并在响应中返回,而 PUT 和 PATCH 在请求中需要 URL。对吗? 【参考方案1】:

我也对此很好奇,发现了一些有趣的文章。我可能无法完全回答您的问题,但这至少提供了更多信息。

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 时,您只需更新您指定的字段,其余部分如您的示例所示。

以下对 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": "new.email@example.org" 
]

您或多或少地将 PATCH 视为更新字段的一种方式。因此,您不是通过部分对象发送,而是通过操作发送。即用价值替换电子邮件。

文章以此结束。

值得一提的是,PATCH 并不是真正为真正的 REST 设计的 API,因为 Fielding 的论文没有定义任何方式来部分 修改资源。但是,罗伊菲尔丁自己说 PATCH 是 [他] 为最初的 HTTP/1.1 提案创建了一些东西,因为 部分 PUT 绝不是 RESTful。确定您没有传输完整的 表示,但 REST 不要求表示 无论如何都要完成。

现在,我不知道我是否特别同意这篇文章,正如许多评论员指出的那样。发送部分表示很容易成为对更改的描述。

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

【讨论】:

可能值得添加:在 William Durand 的文章(和 rfc 6902)中有“op”是“add”的示例。这显然不是幂等的。 或者您可以更轻松地改用 RFC 7396 Merge Patch 并避免构建补丁 JSON。 对于nosql表,patch和put之间的区别很重要,因为nosql没有列【参考方案2】:

注意:当我第一次花时间阅读有关 REST 的内容时,幂等性是一个令人困惑的概念,试图正确理解。正如进一步的 cmets(和Jason Hoetger's answer)所显示的那样,我的原始答案仍然没有完全正确。有一段时间,我一直拒绝广泛更新这个答案,以避免有效地抄袭 Jason,但我现在正在编辑它,因为,嗯,我被要求(在 cmets 中)。

看完我的回答后,我建议你也读一下Jason Hoetger's excellent answer这个问题,我会尽量让我的回答变得更好,而不是简单地从杰森那里偷东西。

为什么 PUT 是幂等的?

正如您在 RFC 2616 引用中所指出的,PUT 被认为是幂等的。当你 PUT 一个资源时,这两个假设在起作用:

    您指的是一个实体,而不是一个集合。

    您提供的实体是完整的(整个实体)。

让我们看一个你的例子。

 "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 只更新提供的字段,而其他的则不理会。​​p>

由于 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不关心email,只关心邮政编码,如果重复申请,也会得到同样的结果:邮政编码设置为新值。 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's answer。我将就此搁置,因为老实说,我认为我无法比他已经回答的更好。

【讨论】:

这句话不太正确:“但它是幂等的:只要 A 进去,B 总是出来”。例如,如果您在邮局更新邮政编码之前发送GET /users/1,然后在邮局更新后再次发出相同的GET /users/1 请求,您将收到两个不同的响应(不同的邮政编码)。相同的“A”(GET 请求)正在进入,但您会得到不同的结果。然而 GET 仍然是幂等的。 @DanLowe:GET 绝对保证是幂等的。它在 RFC 2616 的第 9.1.2 节和更新的规范RFC 7231 section 4.2.2 中准确地说,“在本规范定义的请求方法中,PUT、DELETE 和安全请求方法是幂等的。”幂等性并不意味着“每次发出相同的请求时都会得到相同的响应”。 7231 4.2.2 继续说:“重复请求将具有相同的预期效果,即使原始请求成功,尽管响应可能不同。 @JasonHoetger 我承认这一点,但我不明白它与这个答案有什么关系,它讨论了 PUT 和 PATCH,甚至从未提及 GET... “这个 PUT 请求在技术上是幂等的” - 是的,但它发送了错误的数据(即丢失的数据),这就是重点。好片子。 啊,@JasonHoetger 的评论澄清了这一点:只有多个幂等方法请求的结果状态而不是响应需要相同。【参考方案3】:

虽然 Dan Lowe 的出色回答非常彻底地回答了 OP 关于 PUT 和 PATCH 之间区别的问题,但它对为什么 PATCH 不是幂等问题的回答并不完全正确。

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

幂等这个词更广泛地用于描述一个如果执行一次或多次会产生相同结果的操作[...] 幂等函数是具有属性 f(f(x)) = f( x) 对于任何值 x。

在更易于理解的语言中,幂等 PATCH 可以定义为:使用补丁文档对资源进行 PATCH 后,所有后续对具有相同补丁文档的同一资源的 PATCH 调用都不会更改该资源。

相反,非幂等操作是 f(f(x)) != f(x),对于 PATCH 可以表述为:在使用补丁文档对资源进行 PATCH 之后,随后的 PATCH 调用相同具有相同补丁文档的资源更改资源。

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

[ "id": 1, "username": "firstuser", "email": "firstuser@example.org" ]

假设服务器允许 PATCHing /users,而不是像 OP 的示例中那样修补 /users/id。让我们发出这个 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 请求。

在 Dan 的示例中,他的 PATCH 操作实际上是幂等的。在那个例子中,/users/1 实体在我们的 PATCH 请求之间发生了变化,但不是因为我们的 PATCH 请求;实际上是邮局的不同补丁文件导致了邮政编码的改变。邮局不同的PATCH是不同的操作;如果我们的 PATCH 是 f(x),邮局的 PATCH 是 g(x)。幂等性声明f(f(f(x))) = f(x),但不保证f(g(f(x)))

【讨论】:

假设服务器还允许在/users 发出 PUT,这也会使 PUT 非幂等。这一切都归结为服务器是如何处理请求的。 所以,我们可以只使用 PATCH 操作来构建 API。那么,使用 http VERBS 对 Resources 进行 CRUD 操作的 REST 原则是什么?先生们,我们是不是过于复杂了 PATCH 边界? 如果 PUT 在集合上实现(例如 /users),任何 PUT 请求都应该替换该集合的内容。因此,对/users 的 PUT 应该期望用户集合并删除所有其他用户。这是幂等的。您不太可能在 /users 端点上做这样的事情。但是像/users/1/emails 这样的东西可能是一个集合,并且允许用一个新集合替换整个集合可能是完全有效的。 虽然这个答案提供了幂等性的一个很好的例子,但我相信这可能会使典型 REST 场景中的水变得浑浊。在这种情况下,您有一个 PATCH 请求,其中包含一个触发特定服务器端逻辑的附加 op 操作。这将要求服务器和客户端知道要为 op 字段传递的特定值以触发服务器端工作流。在更直接的 REST 场景中,这种类型的 op 功能是不好的做法,应该直接通过 HTTP 动词处理。 我永远不会考虑针对集合发布 PATCH,只发布 POST 和 DELETE。这真的做过吗?因此,对于所有实际目的,PATCH 是否可以被认为是幂等的?【参考方案4】:

PUT和PATCH的区别在于:

    PUT 必须是幂等的。为了实现这一点,您必须将整个完整的资源放入请求正文中。 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 请求正文中:

    字段的存在意味着“替换”或“添加”该字段。 如果某个字段的值为空,则表示删除该字段。

通过上述约定,示例中的 PATCH 可以采用以下形式:

PATCH /contacts/1

  "address": "123 main street",
  "email": "abc@myemail.com",
  "zip":

这看起来更简洁和用户友好。但用户需要了解底层约定。

通过我上面提到的操作,PATCH 仍然是幂等的。但是如果你定义像“increment”或“append”这样的操作,你可以很容易地看到它不再是幂等的了。

【讨论】:

【参考方案5】:

让我更详细地引用和评论RFC 7231 section 4.2.2,已经在早期的 cmets 中引用:

一个请求方法被认为是“幂等的”,如果预期的效果是 使用该方法的多个相同请求的服务器是相同的 作为单个此类请求的效果。请求方法 本规范定义的 PUT、DELETE 和安全请求方法 是幂等的。

(...)

区分幂等方法是因为请求可以是 如果在通信失败之前发生自动重复 客户端能够读取服务器的响应。例如,如果一个 客户端发送 PUT 请求并关闭底层连接 在收到任何响应之前,客户端可以建立一个新的 连接并重试幂等请求。它知道重复 该请求将具有相同的预期效果,即使原来的 请求成功,但响应可能不同。

那么,在一个幂等方法的重复请求之后,应该是什么“相同”呢?不是服务器状态,也不是服务器响应,而是预期效果。特别是,“从客户端的角度来看”该方法应该是幂等的。现在,我认为这种观点表明Dan Lowe's answer 中的最后一个示例,我不想在这里抄袭,确实表明 PATCH 请求可以是非幂等的(以比示例更自然的方式Jason Hoetger's answer)。

确实,让我们通过为第一个客户明确一个可能的意图来使示例更加精确。假设该客户通过该项目的用户列表检查他们的电子邮件邮政编码。他从用户 1 开始,注意到 zip 是正确的,但电子邮件是错误的。他决定用一个完全合法的 PATCH 请求来纠正这个问题,并且只发送

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

因为这是唯一的更正。现在,由于某些网络问题,请求失败,并在几个小时后自动重新提交。同时,另一个客户端(错误地)修改了用户 1 的 zip。然后,第二次发送相同的 PATCH 请求并没有达到客户端的预期效果,因为我们最终得到了一个不正确的邮编。因此,该方法在 RFC 的意义上不是幂等的。

如果客户端使用 PUT 请求更正电子邮件,将用户 1 的所有属性连同电子邮件一起发送到服务器,即使稍后必须重新发送请求并且用户 1 也可以达到预期的效果同时已被修改 --- 因为第二个 PUT 请求将覆盖自第一个请求以来的所有更改。

【讨论】:

【参考方案6】:

依我的拙见,幂等性意味着:

输入:

我发送了一个完整的资源定义,因此 - 生成的资源状态与 PUT 参数定义的完全一样。每次我使用相同的 PUT 参数更新资源时 - 结果状态完全相同。

补丁:

我只发送了部分资源定义,因此其他用户可能会同时更新此资源的 OTHER 参数。因此 - 具有相同参数及其值的连续补丁可能会导致不同的资源状态。例如:

假设一个对象定义如下:

汽车: - 颜色:黑色, - 类型:轿车, - 座位:5

我修补它:

颜色:'红色'

生成的对象是:

汽车: - 红色, - 类型:轿车, - 座位:5

然后,其他一些用户对这辆车进行了修补:

type: '掀背车'

所以,结果对象是:

汽车: - 红色, - 类型:掀背车, - 座位:5

现在,如果我再次修补这个对象:

颜色:'红色'

结果对象是:

汽车: - 红色, - 类型:掀背车, - 座位:5

和我之前的有什么不同!

这就是为什么 PATCH 不是幂等的,而 PUT 是幂等的。

【讨论】:

【参考方案7】:

TLDR - 简化版

PUT => 为现有资源设置所有新属性。

PATCH => 部分更新现有资源(并非所有属性都需要)。

【讨论】:

另外:PATCH => 可以是说明,而不仅仅是更新的属性【参考方案8】:

为了结束关于幂等性的讨论,我应该指出,可以通过两种方式在 REST 上下文中定义幂等性。让我们首先形式化一些事情:

resource 是一个函数,其 codomain 是字符串类。换句话说,资源是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

(此定义专门用于讨论PUTPOST,例如对GETPOST 没有多大意义,因为它不关心持久性)。

现在,通过修复 x: Res(从信息上讲,使用柯里化),PUT(x: Res)PATCH(x: Res)Res → Res 类型的单变量函数。

    函数g: Res → Res被称为全局幂等,当g ○ g == g,即对于任何y: Resg(g(y)) = g(y)

    x: Res 成为资源,k = x.keys。一个函数g = f(x) 被称为左幂等,当对于每个y: Res,我们有g(g(y))|ₖ == g(y)|ₖ。这基本上意味着如果我们查看应用的键,结果应该是相同的。

所以,PATCH(x) 不是全局幂等的,而是左幂等的。左幂等性在这里很重要:如果我们修补资源的几个键,如果我们再次修补它,我们希望这些键相同,并且我们不关心资源的其余部分。

当 RFC 谈论 PATCH 不是幂等性时,它是在谈论全局幂等性。好吧,它不是全局幂等的就好了,否则它会是一个失败的操作。


现在,Jason Hoetger's answer 试图证明 PATCH 甚至不是左幂等的,但这样做会破坏太多东西:

首先,PATCH 用于集合,尽管 PATCH 被定义为用于映射/字典/键值对象。 如果有人真的想将 PATCH 应用于集合,那么应该使用一个自然的翻译:t: Set<T> → Map<T, Boolean>,用x in A iff t(A)(x) == True 定义。使用此定义,修补是左幂等的。 在示例中,未使用此转换,而是 PATCH 像 POST 一样工作。首先,为什么要为对象生成一个ID?它是什么时候生成的?如果首先将对象与集合的元素进行比较,如果没有找到匹配的对象,则生成 ID,那么程序应该再次以不同的方式工作(id: 1, email: "me@site.com" 必须与 email: "me@site.com" 匹配,否则程序总是坏了,补丁不可能修补)。如果在检查集合之前生成了 ID,则程序再次被破坏。

可以举出 PUT 非幂等的例子,打破了这个例子中被破坏的一半:

生成的附加功能的一个例子是版本控制。人们可能会记录单个对象的更改次数。在这种情况下,PUT 不是幂等的:PUT /user/12 email: "me@site.com" 第一次产生email: "...", version: 1,第二次产生email: "...", version: 2。 与 ID 混淆,可能会在每次更新对象时生成一个新 ID,从而导致非幂等 PUT。

以上所有例子都是人们可能遇到的自然例子。


我的最后一点是,PATCH 不应该是全局幂等的,否则不会给你想要的效果。您希望更改用户的电子邮件地址,而不触及其余信息,并且您不想覆盖访问同一资源的另一方的更改。

【讨论】:

【参考方案9】:

我要补充的一个附加信息是,与 PUT 请求相比,PATCH 请求使用的带宽更少,因为只发送了一部分数据而不是整个实体。因此,只需使用 PATCH 请求更新特定记录(如(1-3 条记录)),而 PUT 请求更新大量数据。就是这样,不要想太多,也不要太担心。

【讨论】:

【参考方案10】:

其他人都回答了 PUT vs PATCH。我只是要回答原始问题标题的哪一部分:“......在 REST API 现实生活场景中”。在现实世界中,这发生在我的互联网应用程序中,该应用程序有一个 RESTful 服务器和一个关系数据库,该数据库具有一个“宽”(大约 40 列)的 Customer 表。我错误地使用了 PUT,但认为它就像一个 SQL 更新命令并且没有填写所有列。问题:1)一些列是可选的(所以空白是有效的答案),2)许多列很少改变,3)一些列不允许用户更改,例如最后购买日期的时间戳,4)一列是免费的-form text "Comments" 栏,用户用半页的客服中心填写,比如配偶姓名询问或通常的订单,5)我当时正在开发一个互联网应用程序,担心数据包大小。

PUT 的缺点是它会迫使您发送大量信息(包括整个评论列在内的所有列,即使只更改了一些内容)以及 2 个以上用户同时编辑同一客户的多用户问题(所以最后一个按更新获胜)。 PATCH 的缺点是您必须在视图/屏幕方面跟踪更改的内容,并具有一些智能来仅发送更改的部分。 Patch 的多用户问题仅限于编辑同一客户的同一列。

【讨论】:

【参考方案11】:

这里有一个很好的解释-

https://blog.segunolalive.com/posts/restful-api-design-%E2%80%94-put-vs-patch/#:~:text=RFC%205789,not%20required%20to%20be%20idempotent.

正常的有效载荷- // 地块 1 上的房子 地址:“地块 1”, 所有者:'segun', 类型:'双工', 颜色:“绿色”, 房间:'5', 厨房:'1', 窗户:20 PUT 更新- // PUT 请求有效载荷以更新地块 1 上的房屋窗口 地址:“地块 1”, 所有者:'segun', 类型:'双工', 颜色:“绿色”, 房间:'5', 厨房:'1', 窗户:21 注意:在上面的有效载荷中,我们正在尝试将窗口从 20 更新到 21。

现在查看 PATH 有效负载 - // 修补请求有效负载以更新房屋上的窗口 窗户:21

由于 PATCH 不是幂等的,失败的请求不会在网络上自动重新尝试。此外,如果对不存在的 url 发出 PATCH 请求,例如尝试替换不存在的建筑物的前门,它应该会失败而不会创建新资源,这与 PUT 不同,后者会使用有效负载创建新资源。想想看,在一个家庭地址上有一扇单独的门会很奇怪。

【讨论】:

【参考方案12】:

tl;dr 版本

POST:用于创建一个实体

PUT:用于更新一个实体,您必须发送该实体的完整表示,以便存储它

PATCH:用于更新只发送需要更新的字段的实体

【讨论】:

以上是关于在 REST API 现实生活场景中使用 PUT 与 PATCH 方法的主要内容,如果未能解决你的问题,请参考以下文章

REST API PATCH或PUT

RESTful API设计中常见的问题和解决方案

如何在 Laravel REST API 中使用 PUT 方法更新图像?

如何在现实生活场景中使用消息队列?

Rest API 设计 PUT vs PATCH

Mailchimp API Put 或 Patch 使用 Delphi REST