REST API – PUT vs PATCH与现实生活中的例子
首先是一些定义:
PUT在第9.6节RFC 2616中定义:
PUT方法要求封闭的实体存储在提供的Request-URI下。 如果Request-URI引用一个已经存在的资源,封闭的实体应该被认为是驻留在原始服务器上的修改版本 。 如果Request-URI不指向现有资源,并且该URI能够被请求用户代理定义为新资源,则源服务器可以使用该URI创build资源。
PATCH在RFC 5789中定义:
PATCH方法请求将请求实体中描述的一组变更应用于由Request-URI标识的资源。
同样根据RFC 2616第9.1.2节, PUT是幂等而PATCH不是。
现在让我们看一个真实的例子。 当我使用数据{username: 'skwee357', email: 'skwee357@domain.com'}
对/users
进行POST并且服务器能够创build资源时,它将响应201和资源位置(让我们假设/users/1
),任何下次调用GET /users/1
将返回{id: 1, username: 'skwee357', email: 'skwee357@domain.com'}
。
现在让我们说我想修改我的电子邮件。 电子邮件修改被认为是“一系列变化”,因此我应该使用“ 补丁文件 ”来修补 PATCH /users/1
。 在我的情况下,这将是一个JSON {email: 'skwee357@newdomain.com'}
。 服务器然后返回200(假设权限是好的)。 这引起我第一个问题:
- 补丁不是幂等的。 它在RFC 2616和RFC 5789中是这样说的。但是,如果我发出相同的PATCH请求(使用我的新电子邮件),我会得到相同的资源状态(将我的电子邮件修改为所请求的值)。 为什么PATCH不是幂等的?
PATCH是一个相对较新的动词(RFC于2010年3月推出),它解决了“修补”或修改一组字段的问题。 在引入PATCH之前,大家都使用PUT来更新资源。 但是在PATCH引入之后,这让我很困惑PUT使用了什么呢? 这使我想到了第二个(也是主要的)问题:
- PUT和PATCH之间的真正区别是什么? 我读过PUT可能被用来取代特定资源下的整个实体的地方,所以应该发送完整的实体(而不是像PATCH那样的属性集合)。 这种情况下真正的实际用法是什么? 什么时候你想replace/覆盖特定资源URI下的实体,以及为什么这样的操作不被认为是更新/修补实体? 我所看到的唯一的实际用例是PUT集合,即
/users
来replace整个集合。 在PATCH引入之后,在特定的实体上发布PUT是没有意义的。 我错了吗?
注意 :当我第一次花时间阅读REST时,幂等性是一个令人困惑的概念,试图find正确的答案。 进一步的评论(和Jason Hoetger的回答 )已经表明,我仍然没有把原来的答案说得很对。 有一段时间,我拒绝广泛地更新这个答案,以避免有效地剽窃杰森,但我现在正在编辑它,因为我被要求(在评论中)。
在阅读我的回答之后,我build议你也阅读贾森·霍格(Jason Hoetger)对这个问题的出色答案 ,我会尽量让自己的回答更好,而不是简单地从杰森那里偷窃。
为什么PUT是幂等的?
正如您在RFC 2616中引用的那样,PUT被认为是幂等的。 当你投入一个资源时,这两个假设正在起作用:
-
你指的是一个实体,而不是一个集合。
-
您提供的实体是完整的( 整个实体)。
我们来看一个例子。
{ "username": "skwee357", "email": "skwee357@domain.com" }
如果您将这个文档发布给/users
,那么您可能会找回像这样的实体
## /users/1 { "username": "skwee357", "email": "skwee357@domain.com" }
如果你想稍后修改这个实体,你可以selectPUT和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的时候,假定你正在发送完整的实体,并且这个完整的实体replace了那个URI的任何现有的实体。 在上面的例子中,PUT和PATCH完成相同的目标:他们都改变这个用户的电子邮件地址。 但是PUT通过replace整个实体来处理它,而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出来,所以这是幂等的。
一个小时之后,在你喝完咖啡rest一下之后,别人会自己贴上补丁。 看来邮局一直在做一些改变。
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进来了,所以这也是幂等的。
第二天,你决定再次发送你的补丁。
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的答案 。 我只是想放弃这一点,因为我真的不认为我可以比他已经有更好的回答这个部分。
虽然Dan Lowe的出色答案非常透彻地回答了OP关于PUT和PATCH之间差异的问题,但是为什么PATCH不是幂等的这个问题的答案并不完全正确。
为了说明为什么PATCH不是幂等的,它有助于开始定义幂等(从维基百科 ):
幂等函数是一个具有f(f(x))= f(x)的性质的函数,用于描述一个将产生相同结果的运算任何值x。
在更容易访问的语言中,可以将幂等PATCH定义为:在用补丁文档对资源进行PATCH之后,所有后续PATCH调用具有相同补丁文档的相同资源将不会改变资源。
相反,非幂等运算是f(f(x))!= f(x),这对于PATCH可以表示为:在用补丁文档修补资源之后,随后的PATCH调用相同的资源同一个补丁文件做更改资源。
为了说明一个非幂等的PATCH,假设有一个/ users资源,并假设调用GET /users
返回一个用户列表,目前为:
[{ "id": 1, "username": "firstuser", "email": "firstuser@example.org" }]
在OP的例子中,假设服务器允许PATCHING /用户,而不是PATCHing / 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不是幂等的 。
尽pipePATCH不是幂等的,但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)))
。
我也很好奇,发现了一些有趣的文章。 我可能不会全面回答你的问题,但至less可以提供更多的信息。
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是按照规范devise的,那么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请求中,封闭的实体被认为是存储在源服务器上的资源的修改版本,并且客户端正在请求replace存储的版本。 但是,对于PATCH,封闭的实体包含一组说明,说明当前驻留在源服务器上的资源如何修改以生成新的版本。 PATCH方法会影响Request-URI所标识的资源,也可能对其他资源产生副作用; 即通过应用PATCH可以创build新的资源,或者修改现有的资源。
PATCH /users/123 [ { "op": "replace", "path": "/email", "value": "new.email@example.org" } ]
您或多或less将PATCH视为更新字段的一种方式。 所以不是发送部分对象,而是发送操作。 即用值replace电子邮件。
文章以此结束。
值得一提的是,PATCH并不是真正为真正的REST APIdevise的,因为Fielding的论文没有定义任何部分修改资源的方法。 但是,Roy Fielding自己说,PATCH是他为最初的HTTP / 1.1提案创build的,因为部分PUT从来就不是RESTful。 当然你并没有完整的表示,但是REST并不需要表示就完成了。
现在,很多评论家指出,我不知道我是否特别同意这篇文章。 通过部分表示发送可以很容易地描述变化。
对我来说,混合使用PATCH。 大多数情况下,我将把PUT当作PATCH来处理,因为迄今为止我唯一注意到的真正区别是PUT“应该”将缺失值设置为null。 这可能不是“最正确”的方法,但是祝好运编码完美。
PUT和PATCH的区别在于:
- PUT需要是幂等的。 为了实现这一点,你必须把整个完整的资源放在请求体中。
- PATCH可以是非幂等的。 这意味着它在某些情况下也可能是幂等的,比如你所描述的情况。
PATCH需要一些“补丁语言”来告诉服务器如何修改资源。 调用者和服务器需要定义一些“操作”,如“添加”,“replace”,“删除”。 例如:
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请求体中:
- 字段的存在意味着“replace”或“添加”该字段。
- 如果一个字段的值为空,则意味着删除该字段。
按照上面的惯例,例子中的PATCH可以采取以下forms:
PATCH /contacts/1 { "address": "123 main street", "email": "abc@myemail.com", "zip": }
这看起来更简洁和用户友好。 但用户需要了解基础约定。
通过上面提到的操作,PATCH仍然是幂等的。 但是,如果你定义的操作如“增量”或“追加”,你可以很容易地看到它不再是幂等的。