当前位置: 首页 > 知识库问答 >
问题:

REST API实际场景中PUT vs PATCH方法的使用

百里业
2023-03-14

首先,一些定义:

PUT在第9.6节RFC 2616中定义:

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

修补程序定义在RFC 5789:

补丁方法请求将请求实体中描述的一组更改应用于请求URI标识的资源。

另外,根据RFC2616第9.1.2节,PUT是幂等的,而补丁不是。

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

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

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

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

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

共有2个答案

艾泽语
2023-03-14

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

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

幂等这个术语被更全面地用来描述一个如果执行一次或多次就会产生相同结果的操作[...]幂等函数是对任意值x具有性质f(f(x))=f(x)的函数。

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

相反地,非幂等操作是f(f(x))!=f(x)的操作,对于补丁可以这样表述:在使用补丁文档对资源进行补丁之后,使用相同的补丁文档对相同资源进行后续的补丁调用确实会更改资源。

为了说明非幂等修补程序,假设存在/users资源,并且假设调用get/users返回用户列表,当前为:

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

假设服务器允许修补/users/{id},而不是像OP的示例中那样修补/users/{id}。让我们发出这个补丁请求:

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" }]

现在,如果我们发出与上面完全相同的补丁请求,会发生什么呢?(为了本例的需要,我们假设/users资源允许重复用户名。)“操作”是“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资源再次更改,尽管我们针对完全相同的端点发布了完全相同的补丁程序。如果我们的补片是f(x),f(f(x))与f(x)不相同,因此,这个特定的补片不是幂等的。

虽然修补程序不能保证是幂等的,但修补程序规范中没有任何内容可以阻止您在特定服务器上进行所有修补程序操作。RFC5789甚至预见到了幂等修补程序请求的优势:

可以以幂等的方式发出补丁请求,这也有助于防止相同资源上的两个补丁请求在相似的时间框架内发生冲突而产生不良结果。

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

昝阳嘉
2023-03-14

注意:当我第一次花时间阅读关于REST的文章时,幂等性是一个让人困惑的概念,试图弄对它。正如进一步的评论(和Jason Hoetger的回答)所表明的那样,我在最初的回答中仍然没有完全正确。有一段时间,我一直拒绝广泛更新这个答案,以避免有效地剽窃Jason,但我现在正在编辑它,因为,嗯,我被要求(在评论中)。

读完我的答案后,我建议你也读一读Jason Hoetger对这个问题的出色回答,我会试着让我的答案更好,而不是简单地从Jason那里偷东西。

正如您在RFC2616引文中指出的,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 /users/1
    {
        "email": "skwee357@gmail.com"       // new email address
    }
    

    你马上就会注意到这两者之间的区别。PUT包含了该用户的所有参数,但修补程序只包含了正在修改的参数(email)。

    在使用PUT时,假设您正在发送完整的实体,并且该完整的实体替换了该URI处的任何现有实体。在上面的例子中,PUT和补丁完成了相同的目标:它们都更改了这个用户的电子邮件地址。但是PUT通过替换整个实体来处理它,而PATCH只更新提供的字段,而不更新其他字段。

    由于PUT请求包括整个实体,如果您重复发出相同的请求,它应该总是具有相同的结果(您发送的数据现在是实体的全部数据)。因此PUT是幂等的。

    如果在PUT请求中使用上述补丁数据,会发生什么情况?

    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请求在技术上是幂等的,但这并不意味着它不是一个可怕的、破碎的想法。

    在上面的例子中,补丁是幂等的。您进行了更改,但如果您一次又一次地进行相同的更改,它总是会回馈相同的结果:您将电子邮件地址更改为新的值。

    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
    }
    

    我最初有一些例子,我认为它们显示了非幂等性,但它们是误导的/不正确的。我将保留这些示例,但使用它们来说明一个不同的事情:针对同一实体的多个补丁文档,修改不同的属性,并不会使补丁非幂等的。

    假设在过去的某个时候,添加了一个用户。这是你开始的状态。

    {
      "id": 1,
      "name": "Sam Kwee",
      "email": "skwee357@olddomain.com",
      "address": "123 Mockingbird Lane",
      "city": "New York",
      "state": "NY",
      "zip": "10001"
    }
    

    补丁后,您就有了一个修改过的实体:

    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"
    }
    

    如果您然后重复应用补丁程序,您将继续得到相同的结果:电子邮件被更改为新值。A进去,A出来,所以这是幂等的。

    一个小时后,当你去泡点咖啡,休息一下后,另一个人带着自己的补丁走了过来。看来邮局一直在做一些改变。

    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
    }
    

    由于这个来自邮局的补丁本身并不关心电子邮件,只关心邮政编码,如果反复应用,也会得到同样的结果:邮政编码被设置为新值。A进去,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请求,并包含您的修改(如果有的话)。所以,当你拿回这个回复时,它和你昨天拿回的不一样,因为邮政编码不是你上次收到的那个。然而,您的请求与邮政编码无关,只与电子邮件有关。所以您的补丁文档仍然是幂等的--您在补丁中发送的电子邮件现在是实体上的电子邮件地址。

    关于这个问题的全部处理,我再次请你参考杰森·霍特格的回答。我就不说了,因为我真的不认为我能比他更好地回答这部分。

  •  类似资料:
    • 问题内容: 首先,一些定义: PUT在9.6节RFC 2616中 定义: PUT方法请求将封闭的实体存储在提供的Request-URI下。如果Request-URI引用了已经存在的资源, 则应 将封闭的实体 视为原始服务器上的 资源 的修改版本 。如果Request- URI没有指向现有资源,并且请求用户代理能够将该URI定义为新资源,则原始服务器可以使用该URI创建资源。 PATCH在RFC 5

    • 本篇为Powershell攻击指南——黑客后渗透之道系列之实战篇,主要介绍的一些实用的利用方式与利用场景和一些实用工具。 在实际的渗透环境中我们利用Powershell的方式无非两种: 使用编码的方式对Powershell命令进行编码之后运行 远程下载Powershell代码之后直接运行 两种方式各有利弊,第一种比较方便直接编码即可执行,第二种需要到一台拥有公网IP的机器,那么在实际的渗透环境中如

    • 本文向大家介绍浅谈java中unmodifiableList方法的应用场景,包括了浅谈java中unmodifiableList方法的应用场景的使用技巧和注意事项,需要的朋友参考一下 java对象中primitive类型变量可以通过不提供set方法保证不被修改,但对象的List成员在提供get方法后,就可以随意add、remove改变其结构,这不是希望的结果。网上看了下,发现Collections

    • WebAssembly 的整体目标 定义了 WebAssembly 适合做什么。哪些是在 Web 平台可以实现的,哪些是非 Web 平台可以实现的。下面给出了一个不完善的无序列表,包括应用/领域/计算等方向,它们可能将从 WebAssembly 中受益的, WebAssamlby 的设计过程中也会将它们做为用例。 在浏览器中 更好的让一些语言和工具可以编译到 Web 平台运行。 图片/视频编辑。

    • 4.1 使用场景 Camel管理端内部定义了很多模型对象。接口接受的数据均是模型对象对应的json。 Camel管理端,提供了界面和接口两种方式。这两种方式均可对Server、Upstream等配置信息进行更改,以及发布配置文件。那么什么场景下应该使用接口呢? 通过界面对配置文件进行更改,是一种可视化操作,非常的直观。界面方式适用于编辑配置信息,如增加location等操作。 如果通过界面进行配置

    • 学件 “学件”(Learnware)一词由南京大学周志华老师原创,学件(Learnware) = 模型(model) + 规约(specification),具有可重用、可演进、可了解的特性。 很多人可能在自己的应用中已经建立了类似的模型,他们也很愿意找到一个地方把这些模型分享出去。这样一来,一个新用户想要应用,也许不用自己去建立一个,而是先到“学件”市场上找一找有没有合适的,拿来直接或修改后使用