首页 文章

REST API - PUT与PATCH的实例

提问于
浏览
438

首先,一些定义:

PUT在_213409中定义:

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

PATCH在_213410中定义:

PATCH方法请求将请求实体中描述的一组更改应用于Request-URI标识的资源 .

同样根据RFC 2616 Section 9.1.2 PUT是Idempotent而PATCH不是 .

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

现在我想说我想修改我的电子邮件 . 电子邮件修改被视为"a set of changes"因此我应该使用“patch document”修补 /users/1 . 在我的情况下,它将是一个json {email: 'skwee357@newdomain.com'} . 然后服务器返回200(假设权限正常) . 这让我想到第一个问题:

  • PATCH不是幂等的 . 它在RFC 2616和RFC 5789中也是这样说的 . 但是如果我'll issue the same PATCH request (with my new email) Ill get the same resource state (with my email being modified to the requested value). Why isn' t PATCH那么幂等?

PATCH是一个相对较新的动词(2010年3月引入的RFC),它解决了“修补”或修改一组字段的问题 . 在引入PATCH之前,每个人都使用PUT来更新资源 . 但是在引入PATCH之后,让我感到困惑的是当时使用的PUT是什么?这让我想到了第二个(也是主要的)问题:

  • PUT和PATCH之间的真正区别是什么?我已经在某处读过PUT可能用于特定资源下的整个实体,因此应该发送完整实体(而不是像PATCH那样发送一组属性) . 这种情况的实际用法是什么?您希望何时替换/覆盖特定资源URI下的实体以及为什么不将此类操作视为更新/修补实体?我在PUT中看到的唯一实际用例是在集合上发布PUT,即 /users 来替换整个集合 . 在引入PATCH之后,在特定实体上发布PUT是没有意义的 . 我错了吗?

5 回答

  • 666

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

    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视为更新字段的方法 . 因此,您不是通过部分对象发送,而是发送操作 . 即用 Value 替换电子邮件 .

    文章以此结尾 .

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

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

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

  • 2

    让我更加密切地引用和评论前面评论中已经引用的RFC 7231 section 4.2.2

    如果使用该方法对服务器的多个相同请求的预期效果与单个此类请求的效果相同,则请求方法被视为“幂等” . 在本规范定义的请求方法中,PUT,DELETE和安全请求方法是幂等的 . (...)区分幂等方法,因为如果在客户端能够读取服务器响应之前发生通信故障,则可以自动重复请求 . 例如,如果客户端发送PUT请求并且在收到任何响应之前关闭了底层连接,则客户端可以 Build 新连接并重试幂等请求 . 它知道重复请求将具有相同的预期效果,即使原始请求成功,尽管响应可能不同 .

    那么,在重复请求幂等方法后应该是什么"the same"?不是服务器状态,也不是服务器响应,而是 the intended effect . 特别是,该方法应该是幂等的"from the point of view of the client" . 现在,我认为这个观点表明Dan Lowe's answer中的最后一个例子,我不想在这里抄袭,确实表明PATCH请求可以是非幂等的(以比Jason Hoetger's answer中的例子更自然的方式) .

    实际上,让我们通过为第一个客户端明确地设置一个 intend 来使示例更加精确 . 假设这个客户端通过该项目的用户列表检查他们的电子邮件 and 邮政编码 . 他从用户1开始,注意到拉链是正确的,但电子邮件是错误的 . 他决定使用完全合法的PATCH请求更正此问题,并仅发送

    PATCH /users/1
    {"email": "skwee357@newdomain.com"}
    

    因为这是唯一的修正 . 现在,由于某些网络问题,请求失败,并在几个小时后自动重新提交 . 同时,另一个客户端(错误地)修改了用户1的zip . 然后,第二次发送相同的PATCH请求没有达到客户端的 intended effect ,因为我们最终得到了一个不正确的zip . 因此,该方法在RFC意义上不是幂等的 .

    相反,如果客户端使用PUT请求来更正电子邮件,将用户1的所有属性与电子邮件一起发送到服务器,即使稍后需要重新发送请求并且用户1已被修改,也将实现其预期效果同时---因为第二个PUT请求将覆盖自第一个请求以来的所有更改 .

  • 68

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

    为了说明PATCH不是幂等的原因,从idempotence的定义开始(从Wikipedia开始):

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

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

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

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

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

    而不是PATCHing / users / ,如在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 返回:

    [{ "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资源再次更改,即使我们针对完全相同的 endpoints 发出了完全相同的PATCH . 如果我们的PATCH是f(x),则f(f(x))与f(x)不同,因此, this particular PATCH is not idempotent .

    虽然PATCH不能保证是幂等的,但是有PATCH规范中没有任何内容可以阻止您对特定服务器上的所有PATCH操作进行幂等操作 . RFC 5789甚至可以预测幂等PATCH请求的优势:

    PATCH请求可以以幂等的方式发出,这也有助于防止在相似时间帧内相同资源上的两个PATCH请求之间的冲突导致的不良结果 .

    在Dan的例子中,他的PATCH操作实际上是幂等的 . 在该示例中,/ users / 1实体在我们的PATCH请求之间改变,但不是因为我们的PATCH请求;实际上邮政局的不同补丁文件导致邮政编码发生变化 . 邮局's different PATCH is a different operation; if our PATCH is f(x), the Post Office'的PATCH是g(x) . Idempotence声明 f(f(f(x))) = f(x) ,但对 f(g(f(x))) 没有任何保证 .

  • 212

    NOTE :当我第一次花时间阅读关于REST时,幂等性是一个令人困惑的概念,试图做到正确 . 我仍然没有在我的原始答案中得到正确的答案,因为进一步的评论(和Jason Hoetger's answer)已经显示出来 . 有一段时间,我拒绝广泛地更新这个答案,以避免有效地剽窃Jason,但我现在正在编辑它,因为,我被问到(在评论中) .

    在看完我的答案之后,我建议你也阅读这个问题,我会尽量让我的答案更好,而不是简单地从杰森那里偷窃 .

    为什么PUT是幂等的?

    正如您在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错误

    如果在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文档,修改不同的属性,不会使PATCHes成为非幂等的 .

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

    {
      "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中发送的电子邮件现在是实体上的电子邮件地址 .

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

    对于这个问题的完整处理,我再次推荐你Jason Hoetger's answer . 我认为我可以比现在更好地回答这一部分 .

  • 10

    PUT和PATCH之间的区别在于:

    • 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请求正文中:

    • 字段的存在意味着"replace"或"add"该字段 .

    • 如果字段的值为null,则表示删除该字段 .

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

    PATCH /contacts/1
    {
      "address": "123 main street",
      "email": "abc@myemail.com",
      "zip":
    }
    

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

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

相关问题