首先,一些定义:
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是Idempotent而PATCH不是。
现在让我们来看一个真实的例子。当我使用数据/users
对{username: 'skwee357', email: 'skwee357@domain.com'}
进行POST并且服务器能够创建资源时,它将使用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是一个相对较新的动词(2010年3月推出的RFC),它解决了修补问题"或修改一组字段。在引入PATCH之前,每个人都使用PUT来更新资源。但是在引入PATCH之后,让我感到困惑的是当时使用的PUT是什么?这让我想到了第二个(也是主要的)问题:
/users
来替换整个集合。在引入PATCH之后,在特定实体上发布PUT是没有意义的。我错了吗?答案 0 :(得分:756)
注意:当我第一次花时间阅读有关REST时,幂等性是一个令人困惑的概念,试图做到正确。在我的原始答案中,我仍然没有做到这一点,正如进一步的评论(和Jason Hoetger's answer)所示。有一段时间,我拒绝广泛地更新这个答案,以避免有效地抄袭杰森,但我现在正在编辑它,因为,我被要求(在评论中)。
在阅读完答案之后,我建议你也阅读Jason Hoetger's excellent answer这个问题,我会尽量让答案更好,而不是简单地从杰森那里偷窃。
正如您在RFC 2616引文中所述,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只更新提供的字段,而不管其他字段。
由于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!
}
(我为了这个问题的目的而假设服务器没有任何特定的必填字段,并且允许这种情况发生......实际情况可能并非如此。)< / p>
由于我们使用了PUT,但只提供了email
,现在这是该实体中唯一的东西。这导致数据丢失。
这个例子仅用于说明目的 - 实际上并没有这样做。这个PUT请求在技术上是幂等的,但这并不意味着它不是一个可怕的,破碎的想法。
在上面的例子中,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并不关心电子邮件,只有邮政编码,如果重复应用,它也会得到相同的结果:邮政编码设置为新值。 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中发送的电子邮件现在是该实体的电子邮件地址。
对于这个问题的完整处理,我再次推荐你Jason Hoetger's answer。我只是想放弃它,因为老实说我不认为我能比现在更好地回答这部分。
答案 1 :(得分:259)
虽然Dan Lowe的优秀答案非常彻底地回答了OP关于PUT和PATCH之间差异的问题,但是对于为什么PATCH不是幂等的问题的答案并不完全正确。
为了说明PATCH不是幂等的原因,从幂等的定义开始(来自Wikipedia):
术语幂等被更全面地用于描述如果执行一次或多次将产生相同结果的操作[...]幂等函数是具有属性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" }]
而不是PATCHing / users / {id},如在OP的示例中,假设服务器允许PATCHing / users。让我们发出这个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
返回:< / p>
[{ "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)。 Idempotence声明f(f(f(x))) = f(x)
,但对f(g(f(x)))
没有任何保证。
答案 2 :(得分: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'}
这将有效地更新电子邮件。 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,随附的实体包含一组 说明资源当前如何驻留在 应修改原始服务器以生成新版本。 PATCH 方法影响Request-URI标识的资源,它也是 可能对其他资源产生副作用;即,新资源可能是 通过应用PATCH创建或修改现有的。
PATCH /users/123
[
{ "op": "replace", "path": "/email", "value": "new.email@example.org" }
]
您或多或少将PATCH视为更新字段的方法。因此,您不是通过部分对象发送,而是发送操作。即用价值替换电子邮件。
文章以此结尾。
值得一提的是,PATCH并不是真正的REST设计 API,因为菲尔丁的论文没有定义任何部分方法 修改资源。但是,罗伊菲尔丁自己说PATCH是 他为最初的HTTP / 1.1提案创建的东西,因为 部分PUT永远不会RESTful。当然你没有转移一个完整的 表示,但REST不需要表示 无论如何都要完成。
现在,我不知道我是否特别同意这篇文章,正如许多评论员指出的那样。发送部分表示可以很容易地描述变化。
对我来说,我在使用PATCH时很混乱。在大多数情况下,我会将PUT视为PATCH,因为到目前为止我注意到的唯一真正的区别是PUT“应该”将缺失值设置为null。它可能不是“最正确”的方式,但好运编码完美。
答案 3 :(得分:12)
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”这样的操作,你就可以很容易地看到它不再是幂等的了。
答案 4 :(得分:3)
让我更详细地引用和评论RFC 7231 section 4.2.2,
如果请求方法具有预期效果,则认为该方法是“幂等的” 使用该方法的多个相同请求的服务器相同 作为单个此类请求的效果。的请求方法 由本规范,PUT,DELETE和安全请求方法定义 是幂等的。
(...)
全能方法之所以能够被区分,是因为请求可以 如果在通信之前发生通信故障,则自动重复 客户端能够读取服务器的响应。例如,如果 客户端发送一个PUT请求,基础连接关闭 在收到任何响应之前,客户端可以建立一个新的 连接,然后重试幂等请求。它知道重复 该请求将具有相同的预期效果,即使原始请求 请求成功,尽管响应可能有所不同。
那么,在反复要求幂等方法之后,“相同”应该是什么?不是服务器状态,也不是服务器响应,而是预期效果。特别地,该方法应该“从客户的角度来看”是等幂的。现在,我认为这种观点表明,Dan Lowe's answer中的最后一个示例(我不想在这里窃)确实表明,PATCH请求可以是非幂等的(比Jason Hoetger's answer中的示例)。
实际上,让我们为第一个客户提供一个明确的意图来使示例更加精确。假设该客户浏览了该项目的用户列表,以查看其电子邮件和邮政编码。他从用户1开始,注意到邮政编码正确,但是电子邮件错误。他决定使用完全合法的PATCH请求来更正此问题,并且仅发送
PATCH /users/1
{"email": "skwee357@newdomain.com"}
因为这是唯一的更正。现在,由于某些网络问题,请求失败,并在几个小时后自动重新提交。同时,另一个客户端(错误地)修改了用户1的zip。然后,第二次发送相同的PATCH请求并没有达到客户端的预期效果,因为我们最终得到了一个邮编不正确。因此,该方法在RFC的意义上不是幂等的。
如果相反,客户端使用PUT请求来更正电子邮件,并将用户1的所有属性与电子邮件一起发送到服务器,则即使稍后必须重新发送请求并重新发送用户1,也可以实现他的预期效果同时进行了修改---因为第二个PUT请求将覆盖自第一个请求以来的所有更改。
答案 5 :(得分:1)
在我的拙见中,幂等的意思是:
我发送了一个竞争资源定义,所以-结果资源状态与PUT参数定义的完全相同。每次我使用相同的PUT参数更新资源时-结果状态完全相同。
我只发送了一部分资源定义,因此其他用户可能同时正在更新此资源的OTHER参数。因此,具有相同参数及其值的连续修补程序可能会导致资源状态不同。例如:
假定一个定义如下的对象:
汽车: -颜色:黑色, -类型:轿车, -座位:5
我用以下方法修补它:
{颜色:“红色”}
结果对象是:
汽车: - 红色, -类型:轿车, -座位:5
然后,其他一些用户使用以下方法修补了该车:
{type:'hatchback'}
所以,结果对象是:
汽车: - 红色, -类型:掀背车, -座位:5
现在,如果我再次使用以下方法修补此对象:
{颜色:“红色”}
生成的对象是:
汽车: - 红色, -类型:掀背车, -座位:5
与以前的内容有何不同!
这就是为什么PATCH不等幂而PUT是等幂的原因。
答案 6 :(得分:1)
要结束关于幂等性的讨论,我应该指出,可以通过两种方式在REST上下文中定义幂等性。首先让我们正式化一些东西:
资源是一个函数,其共域是字符串的类。换句话说,资源是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}
。(此定义专门用于辩论PUT
和POST
,例如,在GET
和POST
上没有太大意义,因为它没有关心持久性)。
现在,通过修复x: Res
(从信息上讲,使用currying),PUT(x: Res)
和PATCH(x: Res)
是类型为Res → Res
的单变量函数。
当g: Res → Res
时,即对于任何g ○ g == g
,y: Res
,函数g(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甚至都不是幂等的,但是这样做却打破了很多事情:
t: Set<T> → Map<T, Boolean>
定义。使用此定义,修补是幂等的。x in A iff t(A)(x) == True
必须与{id: 1, email: "me@site.com"}
匹配,否则程序总是损坏,并且PATCH可能无法打补丁)。如果在检查集合之前生成了ID,则程序再次被破坏。一个例子可以证明PUT是非幂等的,它破坏了该例子中一半的东西:
{email: "me@site.com"}
第一次产生PUT /user/12 {email: "me@site.com"}
,第二次产生{email: "...", version: 1}
。以上所有示例都是人们可能遇到的自然示例。
我的最后一点是,PATCH不应是全局幂等的,否则将无法获得理想的效果。您想要更改用户的电子邮件地址,而不用触摸其余信息,并且您不想覆盖访问同一资源的另一方的更改。
答案 7 :(得分:1)
其他所有人都回答了PUT vs PATCH。我只是要回答原始问题标题的哪一部分要问:“ ...在REST API现实生活场景中”。在现实世界中,这发生在具有RESTful服务器和带有“宽”(约40列)“客户”表的关系数据库的Internet应用程序中。我错误地使用了PUT,但是假设它就像一个SQL Update命令,并且没有填写所有列。问题:1)有些列是可选的(因此空白为有效答案),2)许多列很少更改,3)不允许用户更改某些列,例如“上次购买日期”的时间戳,4)一列是免费的形式的文本“评论”列,用户辛苦地填写了半页的客户服务评论,例如配偶姓名,以询问OR常规订单; 5)我当时正在使用互联网应用,并且担心数据包的大小。
PUT的缺点是,它迫使您发送大量信息(即使只有几处更改,所有列都包括整个“注释”列),并且同时有2个以上用户同时编辑同一位客户的多用户问题(因此按更新的最后一个赢了)。 PATCH的缺点是您必须在更改的视图/屏幕侧保持跟踪,并具有一定的智能才能仅发送更改的部分。补丁程序的多用户问题仅限于编辑同一客户的同一列。
答案 8 :(得分:0)
PUT与PATCH 很棒的解释在这里: https://medium.com/backticks-tildes/restful-api-design-put-vs-patch-4a061aa3ed0b
答案 9 :(得分:0)
PUT =>为现有资源设置所有新属性。
PATCH =>部分更新现有资源(并非所有属性都是必需的)。
答案 10 :(得分:0)
我仅要添加的一个附加信息是,与仅发送一部分数据而不是发送整个实体相比,PATCH请求使用的带宽比PUT请求少。因此,只需使用PATCH请求来更新特定记录(如1-3条记录),而使用PUT请求来更新大量数据。就是这样,不要想太多或担心太多。
答案 11 :(得分:0)
一个很好的解释在这里-
正常有效负载- //地块1上的房子 { 地址:“地块1”, 所有者:“ segun”, 类型:“双工”, 颜色:“绿色”, 房间:'5', 厨房:“ 1”, 窗户:20 } 放置更新- // PUT请求有效载荷以更新图1上房屋的窗户 { 地址:“地块1”, 所有者:“ segun”, 类型:“双工”, 颜色:“绿色”, 房间:'5', 厨房:“ 1”, Windows:21 } 注意:在上述有效负载中,我们尝试将窗口从20更新为21。
现在查看PATH有效负载- //修补请求有效负载以更新房屋上的窗户 { Windows:21 }
由于PATCH不是幂等的,因此失败的请求不会在网络上自动重新尝试。同样,如果对不存在的URL发出了PATCH请求,例如试图替换不存在的建筑物的前门,则它应该简单地失败而不会创建与PUT不同的新资源,后者会使用有效载荷来创建新资源。想一想,在房屋地址里只有一扇门很奇怪。