REST API - PUT与PATCH的实例

Dmi*_*sev 614 rest json http put http-method

首先,一些定义:

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),并且/users/1将返回对GET的任何下一次调用{id: 1, username: 'skwee357', email: 'skwee357@domain.com'}.

现在假设我要修改我的电子邮件.电子邮件修改被视为"一组更改",因此我应该修补/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是没有意义的.我错了吗?

Dan*_*owe 863

注意:当我第一次花时间阅读有关REST时,幂等性是一个令人困惑的概念,试图做到正确.我仍然没有在原来的答案中得到正确的答案,因为进一步的评论(和Jason Hoetger的回答)已经表明了.有一段时间,我拒绝广泛地更新这个答案,以避免有效地剽窃杰森,但我现在正在编辑它,因为,我被要求(在评论中).

在阅读了我的答案后,我建议你也阅读Jason Hoetger对这个问题的出色回答,并且我会尝试更好地回答,而不是简单地从Jason那里偷窃.

为什么PUT是幂等的?

正如您在RFC 2616引文中所述,PUT被认为是幂等的.当您投入资源时,这两个假设在起作用:

  1. 您指的是实体,而不是集合.

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

我们来看看你的一个例子.

{ "username": "skwee357", "email": "skwee357@domain.com" }
Run Code Online (Sandbox Code Playgroud)

如果您/users按照建议将此文档发布到,则可能会返回一个实体,例如

## /users/1

{
    "username": "skwee357",
    "email": "skwee357@domain.com"
}
Run Code Online (Sandbox Code Playgroud)

如果您想稍后修改此实体,请在PUT和PATCH之间进行选择.PUT可能如下所示:

PUT /users/1
{
    "username": "skwee357",
    "email": "skwee357@gmail.com"       // new email address
}
Run Code Online (Sandbox Code Playgroud)

您可以使用PATCH完成相同的操作.这可能看起来像这样:

PATCH /users/1
{
    "email": "skwee357@gmail.com"       // new email address
}
Run Code Online (Sandbox Code Playgroud)

你会发现这两者之间存在差异.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!
}
Run Code Online (Sandbox Code Playgroud)

(我假设为了这个问题的目的,服务器没有任何特定的必填字段,并允许这种情况发生......实际情况可能并非如此.)

由于我们使用了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
}
Run Code Online (Sandbox Code Playgroud)

我的原始例子,修正了准确性

我最初有一些例子,我认为它们显示出非幂等性,但它们具有误导性/不正确性.我将保留示例,但使用它们来说明不同的事情:针对同一实体的多个PATCH文档,修改不同的属性,不会使PATCHes成为非幂等的.

让我们说在过去的某个时候,添加了一个用户.这是你开始的状态.

{
  "id": 1,
  "name": "Sam Kwee",
  "email": "skwee357@olddomain.com",
  "address": "123 Mockingbird Lane",
  "city": "New York",
  "state": "NY",
  "zip": "10001"
}
Run Code Online (Sandbox Code Playgroud)

在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"
}
Run Code Online (Sandbox Code Playgroud)

如果您随后重复应用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
}
Run Code Online (Sandbox Code Playgroud)

由于来自邮局的这个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"
}
Run Code Online (Sandbox Code Playgroud)

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

我原来的答案出了什么问题

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

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

为了全面处理这个问题,我再次向您推荐Jason Hoetger的答案.我只是要离开它,因为老实说,我认为我不能比现在更好地回答这部分.

  • @DanLowe:GET 绝对保证是幂等的。它在 RFC 2616 的第 9.1.2 节和更新的规范 [RFC 7231 第 4.2.2 节](https://tools.ietf.org/html/rfc7231#section-4.2.2) 中准确地说, “在本规范定义的请求方法中,PUT、DELETE 和安全请求方法是幂等的。” 幂等性并不意味着“每次发出相同的请求时都会得到相同的响应”。7231 4.2.2 继续说道:“即使原始请求成功,重复请求也将具有相同的预期效果,**尽管响应可能有所不同。**” (3认同)
  • 这句话不太正确:"但它是幂等的:每当A进去,B总是出来".例如,如果您在邮局更新邮政编码之前要"GET/users/1",然后在邮局更新后再次提出相同的"GET/users/1"请求,您将获得两种不同的响应(不同邮政编码).同样的"A"(GET请求)正在进行中,但是你得到了不同的结果.然而,GET仍然是幂等的. (2认同)
  • 我在这里同意@JasonHoetger - Dan 的答案当然是“好”并且有一些很好的例子。但最终,它在回答问题及其幂等定义方面是完全错误的。 (2认同)
  • 啊,@JasonHoetger 的评论澄清了这一点:多个幂等方法请求的结果状态(而不是响应)需要相同。 (2认同)

Jas*_*ger 299

虽然Dan Lowe的优秀答案非常彻底地回答了OP关于PUT和PATCH之间差异的问题,但是对于为什么PATCH不是幂等的问题的答案并不完全正确.

为了说明PATCH不是幂等的原因,从idenotence的定义开始(来自维基百科):

术语幂等用于更全面地用于描述如果执行一次或多次将产生相同结果的操作[...]幂等函数是具有属性f(f(x))= f(x)的函数任何值x.

在更易于访问的语言中,幂等PATCH可以定义为:在使用补丁文档修补资源之后,对具有相同补丁文档的同一资源的所有后续PATCH调用都不会更改资源.

相反,非幂等操作是f(f(x))!= f(x),其中PATCH可以表示为:在使用补丁文档修补资源之后,后续PATCH调用同一资源相同的补丁文档确实会更改资源.

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

[{ "id": 1, "username": "firstuser", "email": "firstuser@example.org" }]
Run Code Online (Sandbox Code Playgroud)

而不是PATCHing/users/{id},如在OP的示例中,假设服务器允许PATCHing/users.让我们发出这个PATCH请求:

PATCH /users
[{ "op": "add", "username": "newuser", "email": "newuser@example.org" }]
Run Code Online (Sandbox Code Playgroud)

我们的补丁文档指示服务器将新用户添加到用户newuser列表中.在第一次调用它之后,GET /users将返回:

[{ "id": 1, "username": "firstuser", "email": "firstuser@example.org" },
 { "id": 2, "username": "newuser", "email": "newuser@example.org" }]
Run Code Online (Sandbox Code Playgroud)

现在,如果我们发出与上面完全相同的 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" }]
Run Code Online (Sandbox Code Playgroud)

/ 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).Idempotence指出f(f(f(x))) = f(x),但不保证f(g(f(x))).

  • 因此,我们只能使用PATCH操作构建API.那么,什么成为使用http VERBS在资源上进行CRUD操作的REST原则?我们不是在这里过于复杂的PATCH边界绅士吗? (12认同)
  • 假设服务器还允许在`/ users`发出PUT,这也会使PUT成为非幂等的.所有这些都归结为服务器如何处理请求. (11认同)
  • 这个示例并没有真正遵循正确的 RESTful 语义。该集合不会附加到 PATCH 或 PUT,而是附加到 POST。演示的 `PATCH /users [{ "op": "add", ...` 操作实际上是变相的 `POST /users` 操作。我会重新设计这个 API 并避免任何类似于 RPC-in-JSON 语义的内容。 (7认同)
  • 我绝不会考虑针对集合发布PATCH,只发布POST和DELETE.这真的有过吗?因此,PATCH可以被认为是所有实际目的的幂等因素吗? (5认同)
  • @UzairSajid - PATCH 和 PUT 是独立的。向 /users 发出 PUT 应该始终是幂等的,如果不是,则服务器不合规。 (4认同)
  • 如果在集合上实现PUT(例如`/ users`),则任何PUT请求都应该替换该集合的内容.所以对`/ users`的PUT应该期望用户集合并删除所有其他用户.这是幂等的.你不可能在/ users端点上做这样的事情.但是像`/ users/1/emails`这样的东西可能是一个集合,允许用新的集合替换整个集合可能是完全有效的. (4认同)
  • 虽然这个答案提供了幂等性的一个很好的例子,但我相信这可能会使典型REST场景中的水域变得混乱.在这种情况下,您有一个PATCH请求,其中包含一个触发特定服务器端逻辑的"op"操作.这将要求服务器和客户端知道要传递给"op"字段的特定值以触发服务器端工作流.在更直接的REST场景中,这种类型的"op"功能是不好的做法,应该可以通过HTTP动词直接处理. (4认同)
  • 完全同意@bohr的观点,很容易滥用PATCH。我建议阅读这篇文章[请。不要像白痴那样打补丁。](William Durand)和[RFC 7396](https://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/) ://tools.ietf.org/html/rfc7396)。可悲的是,HTTP是一个巨大的混乱局面,而基于它的REST使其难以忍受。 (2认同)
  • 但是,为什么要使用PATCH将用户添加到用户集合中?您基本上是在创建新资源(新用户),难道不应该通过POST请求来完成吗?这使我感到困惑。 (2认同)
  • HTTP 动词概念和定义是独立的。REST只是一种架构风格。现在,如果 OP 询问如何以 RESTful 方式实现 PATCH 操作,那么其中一些评论可能是有意义的。问题是人们总是混淆这些不一定相关的概念。 (2认同)

Kal*_*ade 70

我对此也很好奇,并发现了一些有趣的文章.我可能不会完全回答你的问题,但这至少提供了一些信息.

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

HTTP RFC指定PUT必须将全新的资源表示作为请求实体.这意味着,如果仅提供某些属性,则应删除这些属性(即设置为null).

鉴于此,PUT应该发送整个对象.例如,

/users/1
PUT {id: 1, username: 'skwee357', email: 'newemail@domain.com'}
Run Code Online (Sandbox Code Playgroud)

这将有效地更新电子邮件.PUT可能不太有效的原因是你唯一真正修改一个字段并包含用户名是没用的.下一个例子显示了差异.

/users/1
PUT {id: 1, email: 'newemail@domain.com'}
Run Code Online (Sandbox Code Playgroud)

现在,如果PUT是根据规范设计的,那么PUT会将用户名设置为null,您将获得以下内容.

{id: 1, username: null, email: 'newemail@domain.com'}
Run Code Online (Sandbox Code Playgroud)

使用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": "new.email@example.org" }
]
Run Code Online (Sandbox Code Playgroud)

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

文章以此结尾.

值得一提的是,PATCH并非真正设计用于真正的REST API,因为Fielding的论文没有定义任何部分修改资源的方法.但是,Roy Fielding自己说PATCH是他为最初的HTTP/1.1提案创建的东西,因为部分PUT永远不会RESTful.当然,您没有转移完整的表示,但REST无论如何都不需要表示完整.

现在,我不知道我是否特别同意这篇文章,正如许多评论员指出的那样.发送部分表示可以很容易地描述变化.

对我来说,我使用PATCH混合使用.在大多数情况下,我会将PUT视为PATCH,因为到目前为止我注意到的唯一真正的区别是PUT"应该"将缺失值设置为null.它可能不是"最正确"的方式,但好运编码完美.

  • 值得补充的是:在William Durand的文章(和rfc 6902)中,有些例子中"op"是"add".这显然不是幂等的. (7认同)
  • 或者您可以更轻松地使用RFC 7396 Merge Patch,避免构建补丁JSON. (2认同)

Bij*_*jan 65

TLDR - 简化版

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

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

  • 另外: PATCH => 可以是指令而不仅仅是更新的属性 (4认同)
  • `PUT` 不要求资源存在。虽然使用“POST”创建并使用“PUT”更新是一种常见模式,但 [RFC](https://www.rfc-editor.org/rfc/rfc9110.html#name-put) 说“PUT方法请求创建目标资源的状态或用请求消息内容中包含的表示定义的状态替换目标资源的状态。” 您可以很好地设计一个 API,以便“PUT /users/1 HTTP/1.1”将创建 ID 为 1 的用户(如果不存在),如果存在则将其替换为 ID 1。 (4认同)

Bil*_*lal 64

TL;博士版本

  • POST:用于创建实体

  • PUT:用于更新/替换现有实体,您必须按照您希望存储的方式发送该实体的完整表示形式

  • PATCH:用于更新实体,您仅发送需要更新的字段


Bin*_* Ni 14

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",
}
Run Code Online (Sandbox Code Playgroud)

补丁语言不是使用显式的"操作"字段,而是通过定义以下约定来使其隐式:

在PATCH请求体中:

  1. 字段的存在意味着"替换"或"添加"该字段.
  2. 如果字段的值为null,则表示删除该字段.

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

PATCH /contacts/1
{
  "address": "123 main street",
  "email": "abc@myemail.com",
  "zip":
}
Run Code Online (Sandbox Code Playgroud)

这看起来更简洁,用户友好.但用户需要了解基本惯例.

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


小智 8

在我看来,幂等性意味着:

  • 放:

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

  • 修补:

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

假设一个对象定义如下:

CAR:
 - color: black,
 - type: sedan,
 - seats: 5
Run Code Online (Sandbox Code Playgroud)

我用以下方法修补它:

{color: 'red'}
Run Code Online (Sandbox Code Playgroud)

结果对象是:

CAR:
 - color: red,
 - type: sedan,
 - seats: 5
Run Code Online (Sandbox Code Playgroud)

然后,其他一些用户用以下方法修补了这辆车:

{type: 'hatchback'}
Run Code Online (Sandbox Code Playgroud)

所以,得到的对象是:

CAR:
 - color: red,
 - type: hatchback,
 - seats: 5
Run Code Online (Sandbox Code Playgroud)

现在,如果我再次使用以下命令修补该对象:

{color: 'red'}
Run Code Online (Sandbox Code Playgroud)

结果对象是:

CAR:
 - color: red,
 - type: hatchback,
 - seats: 5
Run Code Online (Sandbox Code Playgroud)

和我之前得到的有什么不同!

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


Chr*_*ard 5

考虑到您关于幂等性的问题,我可能有点偏离主题,但我希望您考虑进化性。

假设您有以下元素:

{
  "username": "skwee357",
  "email": "skwee357@domain.example"
}
Run Code Online (Sandbox Code Playgroud)

如果使用 PUT 进行修改,则必须给出对象的完整表示:

PUT /users/1
{
  "username": "skwee357",
  "email": "skwee357@newdomain.example"
}
Run Code Online (Sandbox Code Playgroud)

现在您更新架构并添加一个字段phone

PUT /users/1
{
  "username": "skwee357",
  "email": "skwee357@newdomain.example",
  "phone": "123-456-7890"
}
Run Code Online (Sandbox Code Playgroud)

现在以同样的方式再次使用 PUT 更新它,它将设置phone为 null。为了避免这种不良的副作用,您必须在每次更新架构时更新所有修改元素的组件。瘸。

通过使用 PATCH,就不会出现此问题,因为 PATCH 只更新给定的字段。所以,在我看来,你应该使用 PATCH 来修改一个元素(无论它是否真的幂等)。这就是现实生活中的经验回报。