首先,一些定义:
PUT的定义见章节9.6 RFC 2616:
PUT方法要求将所包含的实体存储在所提供的Request-URI下。如果Request-URI引用了一个已经存在的资源,那么所包含的实体应该被认为是原始服务器上的实体的修改版本。如果Request-URI不指向现有资源,并且请求用户代理能够将该URI定义为新资源,则源服务器可以使用该URI创建资源。
PATCH在RFC 5789中定义:
方法中描述的一组更改
请求实体应用于由request -标识的资源
URI。
此外,根据RFC 2616节9.1.2 PUT是等幂的,而PATCH不是。
现在让我们看一个真实的例子。当我用数据{用户名:'skwee357',电子邮件:'skwee357@domain.example'} POST到/users时,服务器能够创建资源,它将响应201和资源位置(让我们假设/users/1),任何下一次调用GET /users/1将返回{id: 1,用户名:'skwee357',电子邮件:'skwee357@domain.example'}。
现在假设我想修改我的电子邮件。邮件修改被认为是“一组更改”,因此我应该用“补丁文档”PATCH /users/1。在我的例子中,它将是JSON文档:{email: 'skwee357@newdomain.example'}。然后服务器返回200(假设权限正常)。这让我想到了第一个问题:
PATCH不是等幂的。RFC 2616和RFC 5789都是这么说的。但是,如果我发出相同的PATCH请求(用我的新电子邮件),我将获得相同的资源状态(我的电子邮件被修改为所请求的值)。为什么PATCH不是幂等的?
PATCH是一个相对较新的动词(RFC于2010年3月引入),它用来解决“修补”或修改一组字段的问题。在PATCH引入之前,每个人都使用PUT来更新资源。但是在引入PATCH之后,它让我对PUT的用途感到困惑。这就引出了我的第二个(也是主要的)问题:
What is the real difference between PUT and PATCH? I have read somewhere that PUT might be used to replace entire entity under specific resource, so one should send the full entity (instead of set of attributes as with PATCH). What is the real practical usage for such case? When would you like to replace / overwrite an entity at a specific resource URI and why is such an operation not considered updating / patching the entity? The only practical use case I see for PUT is issuing a PUT on a collection, i.e. /users to replace the entire collection. Issuing PUT on a specific entity makes no sense after PATCH was introduced. Am I wrong?
PUT和PATCH的区别在于:
PUT必须是等幂的。为了实现这一点,您必须将整个完整的资源放在请求体中。
PATCH可以是非等幂的。这意味着在某些情况下它也可以是等幂的,比如你描述的情况。
PATCH需要一些“补丁语言”来告诉服务器如何修改资源。调用方和服务器需要定义一些“操作”,如“添加”、“替换”、“删除”。例如:
GET /contacts/1
{
"id": 1,
"name": "Sam Kwee",
"email": "skwee357@olddomain.example",
"state": "NY",
"zip": "10001"
}
PATCH /contacts/1
{
[{"operation": "add", "field": "address", "value": "123 main street"},
{"operation": "replace", "field": "email", "value": "abc@myemail.example"},
{"operation": "delete", "field": "zip"}]
}
GET /contacts/1
{
"id": 1,
"name": "Sam Kwee",
"email": "abc@myemail.example",
"state": "NY",
"address": "123 main street",
}
而不是使用显式的“操作”字段,补丁语言可以通过定义如下约定使其隐式:
PATCH请求体:
字段的存在意味着“替换”或“添加”该字段。
如果字段的值为空,则表示删除该字段。
使用上述约定,示例中的PATCH可以采用以下形式:
PATCH /contacts/1
{
"address": "123 main street",
"email": "abc@myemail.example",
"zip":
}
这看起来更简洁和用户友好。但是用户需要了解底层的约定。
通过上面提到的运算,PATCH仍然是幂等的。但如果你定义像"increment"或"append"这样的操作,你可以很容易地看到它不再是幂等的。
让我更详细地引用和评论RFC 7231第4.2.2节,在之前的评论中已经引用过:
A request method is considered "idempotent" if the intended effect on
the server of multiple identical requests with that method is the same
as the effect for a single such request. Of the request methods
defined by this specification, PUT, DELETE, and safe request methods
are idempotent.
(...)
Idempotent methods are distinguished because the request can be
repeated automatically if a communication failure occurs before the
client is able to read the server's response. For example, if a
client sends a PUT request and the underlying connection is closed
before any response is received, then the client can establish a new
connection and retry the idempotent request. It knows that repeating
the request will have the same intended effect, even if the original
request succeeded, though the response might differ.
那么,在反复要求幂等方法之后,什么应该是“相同的”呢?不是服务器状态,也不是服务器响应,而是预期的效果。特别是,从客户的角度来看,该方法应该是幂等的。现在,我认为这个观点表明了Dan Lowe回答中的最后一个例子,我不想在这里抄袭,它确实表明PATCH请求可以是非幂等的(以一种比Jason Hoetger回答中的例子更自然的方式)。
实际上,让我们为第一个客户机明确一个可能的意图,从而使示例稍微精确一些。假设这个客户端遍历项目的用户列表,以检查他们的电子邮件和邮政编码。他从用户1开始,注意到zip是正确的,但电子邮件是错误的。他决定用PATCH请求来纠正这个问题,这个请求是完全合法的,只发送
PATCH /users/1
{"email": "skwee357@newdomain.example"}
因为这是唯一的修正。现在,由于一些网络问题,请求失败了,并在几个小时后自动重新提交。与此同时,另一个客户端(错误地)修改了用户1的zip文件。然后,第二次发送相同的PATCH请求并不能达到客户端的预期效果,因为我们最终得到了不正确的zip。因此,在RFC的意义上,该方法不是幂等的。
如果客户端使用PUT请求来更正电子邮件,将用户1的所有属性连同电子邮件一起发送给服务器,即使稍后必须重新发送请求并且同时修改了用户1,他也将达到预期的效果——因为第二个PUT请求将覆盖自第一个请求以来的所有更改。
PUT和PATCH的区别在于:
PUT必须是等幂的。为了实现这一点,您必须将整个完整的资源放在请求体中。
PATCH可以是非等幂的。这意味着在某些情况下它也可以是等幂的,比如你描述的情况。
PATCH需要一些“补丁语言”来告诉服务器如何修改资源。调用方和服务器需要定义一些“操作”,如“添加”、“替换”、“删除”。例如:
GET /contacts/1
{
"id": 1,
"name": "Sam Kwee",
"email": "skwee357@olddomain.example",
"state": "NY",
"zip": "10001"
}
PATCH /contacts/1
{
[{"operation": "add", "field": "address", "value": "123 main street"},
{"operation": "replace", "field": "email", "value": "abc@myemail.example"},
{"operation": "delete", "field": "zip"}]
}
GET /contacts/1
{
"id": 1,
"name": "Sam Kwee",
"email": "abc@myemail.example",
"state": "NY",
"address": "123 main street",
}
而不是使用显式的“操作”字段,补丁语言可以通过定义如下约定使其隐式:
PATCH请求体:
字段的存在意味着“替换”或“添加”该字段。
如果字段的值为空,则表示删除该字段。
使用上述约定,示例中的PATCH可以采用以下形式:
PATCH /contacts/1
{
"address": "123 main street",
"email": "abc@myemail.example",
"zip":
}
这看起来更简洁和用户友好。但是用户需要了解底层的约定。
通过上面提到的运算,PATCH仍然是幂等的。但如果你定义像"increment"或"append"这样的操作,你可以很容易地看到它不再是幂等的。
考虑到你关于等幂的问题,我可能有点跑题了,但我希望你考虑一下进化论。
假设你有以下元素:
{
"username": "skwee357",
"email": "skwee357@domain.example"
}
如果你使用PUT进行修改,你必须给出对象的完整表示:
PUT /users/1
{
"username": "skwee357",
"email": "skwee357@newdomain.example"
}
现在更新模式,并添加一个现场电话:
PUT /users/1
{
"username": "skwee357",
"email": "skwee357@newdomain.example",
"phone": "123-456-7890"
}
现在用PUT以同样的方式再次更新它,它将设置phone为null。为了避免这种坏的副作用,您必须在每次更新模式时更新所有修改元素的组件。站不住脚的。
使用PATCH就不会有这个问题,因为PATCH只更新给定的字段。因此,在我看来,您应该使用PATCH来修改一个元素(无论它是否真的是幂等的)。这是现实生活中经验的回归。