关于json:REST API – PUT vs PATCH与现实生活中的例子

REST API - PUT vs PATCH with real life examples

首先,一些定义:

Put定义见第9.6节RFC 2616:

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.

补丁在RFC 5789中定义:

The PATCH method requests that a set of changes described in the
request entity be applied to the resource identified by the Request-
URI.

同样,根据RFC2616第9.1.2节,Put是等幂的,而Patch不是。

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

现在假设我想修改我的电子邮件。电子邮件修改被认为是"一组更改",因此我应该用"补丁文档"来修补/users/1。在我的例子中,它将是一个JSON{email: '[email protected]'}。然后服务器返回200(假设权限正常)。这让我想到了第一个问题:

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

patch是一个相对较新的动词(rfc在2010年3月引入),它用来解决"修补"或修改一组字段的问题。在引入补丁之前,每个人都使用put来更新资源。但在介绍了补丁之后,它让我搞不清当时使用的是什么?这就引出了第二个(也是主要的)问题:

  • Put和Patch的真正区别是什么?我在某个地方读到了Put可以用来替换特定资源下的整个实体,所以应该发送完整的实体(而不是像patch那样的一组属性)。这种情况的实际用法是什么?您希望何时替换/覆盖特定资源URI下的实体?为什么不将此类操作视为更新/修补实体?我看到的唯一实用的Put用例是发布一个Put-on集合,即/users来替换整个集合。在引入修补程序后,发布Put-on特定实体毫无意义。我错了吗?


注意:当我第一次花时间阅读关于休息的内容时,求对的等幂是一个令人困惑的概念。正如更多评论(以及杰森·赫特格的回答)所显示的那样,我在最初的回答中仍然没有得到完全正确的答案。有一段时间,我一直拒绝更新这个答案,以避免有效地抄袭杰森,但我现在正在编辑它,因为,好吧,我被要求(在评论中)。好的。

在阅读了我的答案之后,我建议你也阅读杰森·赫特格对这个问题的完美回答,我将努力使我的答案更好,而不是简单地从杰森那里窃取。好的。为什么是等幂?

正如你在你的RFC2616引文中提到的,Put被认为是等幂的。当您放置资源时,这两个假设起作用:好的。

  • 您所指的是实体,而不是集合。好的。

  • 您提供的实体是完整的(整个实体)。好的。

  • 让我们来看一个例子。好的。

    1
    {"username":"skwee357","email":"[email protected]" }

    如果您按照您的建议将此文档发送到/users,那么您可能会返回一个实体,例如好的。

    1
    2
    3
    4
    5
    6
    ## /users/1

    {
       "username":"skwee357",
       "email":"[email protected]"
    }

    如果您以后想修改这个实体,可以在Put和Patch之间进行选择。看跌期权可能如下:好的。

    1
    2
    3
    4
    5
    PUT /users/1
    {
       "username":"skwee357",
       "email":"[email protected]"       // new email address
    }

    您可以使用补丁来完成相同的工作。可能是这样的:好的。

    1
    2
    3
    4
    PATCH /users/1
    {
       "email":"[email protected]"       // new email address
    }

    你会立刻注意到这两者之间的区别。Put包含了这个用户上的所有参数,但是补丁只包括正在修改的参数(email)。好的。

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

    因为Put请求包括整个实体,所以如果您重复发出相同的请求,它应该始终具有相同的结果(您发送的数据现在是实体的整个数据)。所以put是等幂的。好的。使用Put错误

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    GET /users/1
    {
       "username":"skwee357",
       "email":"[email protected]"
    }
    PUT /users/1
    {
       "email":"[email protected]"       // new email address
    }

    GET /users/1
    {
       "email":"[email protected]"      // new email address... and nothing else!
    }

    (我假设在这个问题中,服务器没有任何特定的必需字段,并且允许发生这种情况……事实上可能并非如此。)好的。

    因为我们使用了Put,但只提供了email,所以这是这个实体中唯一的东西。这导致数据丢失。好的。

    这里的这个例子是为了说明的目的——不要真的这样做。这个Put请求在技术上是等幂的,但这并不意味着它不是一个糟糕的、破碎的想法。好的。怎么是幂等的补丁吗?

    在上述的例子,补丁是幂等的。如果你让你的变化,该变化是再次和再次,它总是相同的结果将反馈到你改变了电子邮件地址的新值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    GET /users/1
    {
       "username":"skwee357",
       "email":"[email protected]"
    }
    PATCH /users/1
    {
       "email":"[email protected]"       // new email address
    }

    GET /users/1
    {
       "username":"skwee357",
       "email":"[email protected]"       // email address was changed
    }
    PATCH /users/1
    {
       "email":"[email protected]"       // new email address... again
    }

    GET /users/1
    {
       "username":"skwee357",
       "email":"[email protected]"       // nothing changed since last GET
    }

    我原来的例子,是固定的.

    我有我的思想originally的例子是显示非幂等的,但他们是misleading /不正确的。我要把他们的例子,但使用不同的东西:这说明该补丁文件的多对不同属性的实体,修改补丁,不让非幂等的。

    让我们说,在过去的一些时间,这是由用户。这是从你在启动状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
     "id": 1,
     "name":"Sam Kwee",
     "email":"[email protected]",
     "address":"123 Mockingbird Lane",
     "city":"New York",
     "state":"NY",
     "zip":"10001"
    }

    你有一个补丁后,改性的实体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PATCH /users/1
    {"email":"[email protected]"}

    {
     "id": 1,
     "name":"Sam Kwee",
     "email":"[email protected]",    // the email changed, yay!
     "address":"123 Mockingbird Lane",
     "city":"New York",
     "state":"NY",
     "zip":"10001"
    }

    如果你的申请,然后你repeatedly补丁,你将继续得到相同的结果:电子邮件是改变到新的价值。在去,出来,因此这是幂等的。

    一小时后,你离开后,使一些咖啡和看别人吃的断裂,随着自己的补丁。看来邮局已制作的一些变化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PATCH /users/1
    {"zip":"12345"}

    {
     "id": 1,
     "name":"Sam Kwee",
     "email":"[email protected]",  // still the new email you set
     "address":"123 Mockingbird Lane",
     "city":"New York",
     "state":"NY",
     "zip":"12345"                      // and this change as well
    }

    因为这个补丁是不是从邮局本身只关注与电子邮件,邮递区号,如果它是repeatedly应用,它也将得到相同的结果:邮编被设置为新值。在去,出来,也因此这是幂等的。

    接下来的一天你的补丁,你决定再次发送。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PATCH /users/1
    {"email":"[email protected]"}

    {
     "id": 1,
     "name":"Sam Kwee",
     "email":"[email protected]",
     "address":"123 Mockingbird Lane",
     "city":"New York",
     "state":"NY",
     "zip":"12345"
    }

    你的补丁有相同的效应:它集自然昨天电子邮件地址。对了,这是一个凸轮10,因此,幂等的目的。

    我要在我的原始错误的答案

    我想画一个重要的区别(我有在我的原始错误的答案)。许多服务器将响应发送回请求用你的休息,你的状态和新的实体,修改(如有)。所以,当你把这个响应后,它是从一个不同的你要回到昨天,因为你不是一个ZIP代码是收到的最后一次。然而,你不需要请求是只与的邮编,电子邮件。你仍然是操作系统的补丁文件的电子邮件地址是幂等的补丁是在现在的电子邮件地址的实体。

    当不操作系统补丁是幂等的,然后呢?

    这是一个完整的治疗,提供了hoetger杰森回答你的。我只是去把它在那,因为我真的认为我可以回答这部分已经比他。

    好的。


    虽然丹·洛的出色回答非常彻底地回答了OP关于Put和Patch之间的区别的问题,但它对Patch为什么不是等幂的问题的回答并不完全正确。

    为了说明补丁不是等幂的原因,可以从等幂的定义开始(从维基百科):

    The term idempotent is used more comprehensively to describe an operation that will produce the same results if executed once or multiple times [...] An idempotent function is one that has the property f(f(x)) = f(x) for any value x.

    在更易访问的语言中,等量修补程序可以定义为:使用修补程序文档修补资源后,对具有相同修补程序文档的同一资源的所有后续修补程序调用都不会更改资源。

    相反,非等幂运算是其中f(f(x))!=f(x),对于patch,可以声明为:在使用修补程序文档修补资源之后,对具有相同修补程序文档的同一资源的后续修补程序调用会更改资源。

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

    1
    [{"id": 1,"username":"firstuser","email":"[email protected]" }]

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

    1
    2
    PATCH /users
    [{"op":"add","username":"newuser","email":"[email protected]" }]

    我们的补丁文件指示服务器将名为newuser的新用户添加到用户列表中。在第一次调用之后,GET /users将返回:

    1
    2
    [{"id": 1,"username":"firstuser","email":"[email protected]" },
     {"id": 2,"username":"newuser","email":"[email protected]" }]

    现在,如果我们发布与上面完全相同的补丁请求,会发生什么?(在本例中,假设/user s资源允许重复的用户名。)"op"是"add",因此将新用户添加到列表中,随后的GET /users返回:

    1
    2
    3
    [{"id": 1,"username":"firstuser","email":"[email protected]" },
     {"id": 2,"username":"newuser","email":"[email protected]" },
     {"id": 3,"username":"newuser","email":"[email protected]" }]

    /users资源再次发生了变化,尽管我们针对完全相同的端点发布了完全相同的补丁。如果我们的补丁是f(x),f(f(x))与f(x)是不同的,因此,这个特定的补丁不是等幂的。

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

    A PATCH request can be issued in such a way as to be idempotent,
    which also helps prevent bad outcomes from collisions between two
    PATCH requests on the same resource in a similar time frame.

    在丹的例子中,他的补丁操作实际上是等幂的。在这个例子中,/users/1实体在我们的补丁请求之间发生了变化,但并不是因为我们的补丁请求;实际上是邮局的不同补丁文档导致邮政编码发生了变化。邮局的不同补丁是不同的操作;如果我们的补丁是f(x),邮局的补丁是g(x)。等幂表示f(f(f(x))) = f(x),但不保证f(g(f(x)))


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

    http://restful-api-design.readthedocs.org/en/latest/methods.html网站

    The HTTP RFC specifies that PUT must take a full new resource
    representation as the request entity. This means that if for example
    only certain attributes are provided, those should be remove (i.e. set
    to null).

    既然如此,那么Put应该发送整个对象。例如,

    1
    2
    /users/1
    PUT {id: 1, username: 'skwee357', email: '[email protected]'}

    这将有效地更新电子邮件。Put可能不太有效的原因是您只修改了一个字段,并且包含了用户名,这有点无用。下一个示例显示了差异。

    1
    2
    /users/1
    PUT {id: 1, email: '[email protected]'}

    现在,如果Put是根据规范设计的,那么Put会将用户名设置为空,然后返回以下内容。

    1
    {id: 1, username: null, email: '[email protected]'}

    使用修补程序时,只更新指定的字段,其余字段保持不变,如示例中所示。

    下面的补丁有点不同于我以前从未见过的。

    网址:http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/

    The difference between the PUT and PATCH requests is reflected in the
    way the server processes the enclosed entity to modify the resource
    identified by the Request-URI. In a PUT request, the enclosed entity
    is considered to be a modified version of the resource stored on the
    origin server, and the client is requesting that the stored version be
    replaced. With PATCH, however, the enclosed entity contains a set of
    instructions describing how a resource currently residing on the
    origin server should be modified to produce a new version. The PATCH
    method affects the resource identified by the Request-URI, and it also
    MAY have side effects on other resources; i.e., new resources may be
    created, or existing ones modified, by the application of a PATCH.

    1
    2
    3
    4
    5
    PATCH /users/123

    [
        {"op":"replace","path":"/email","value":"[email protected]" }
    ]

    您或多或少将补丁作为更新字段的一种方法。因此,不是发送部分对象,而是发送操作。也就是说,用值替换电子邮件。

    文章到此结束。

    It is worth mentioning that PATCH is not really designed for truly REST
    APIs, as Fielding's dissertation does not define any way to partially
    modify resources. But, Roy Fielding himself said that PATCH was
    something [he] created for the initial HTTP/1.1 proposal because
    partial PUT is never RESTful. Sure you are not transferring a complete
    representation, but REST does not require representations to be
    complete anyway.

    现在,我不知道我是否特别同意这篇文章,正如许多评论家指出的那样。发送部分表示可以很容易地描述更改。

    对我来说,我对补丁的使用感到困惑。在大多数情况下,我将把put当作一个补丁,因为到目前为止我注意到的唯一真正区别是,put"should"将缺少的值设置为空。这可能不是最"正确"的方法,但祝您编码完美。


    Put和Patch的区别在于:

  • Put必须是等幂的。为了实现这一点,您必须将整个完整的资源放到请求主体中。
  • 补丁可以是非等幂的。这意味着它在某些情况下也可以是等幂的,比如您描述的情况。
  • 补丁需要一些"补丁语言"来告诉服务器如何修改资源。调用者和服务器需要定义一些"操作",如"添加"、"替换"、"删除"。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    get/contacts/1{"ID":1,"name":"Sam Kwee","email":"[email protected]","state":"ny","zip":"10001"}补丁/联系人/1{["operation":"add","field":"address","value":"123主街<hr><P>让我更详细地引用和评论RFC7231第4.2.2节,已经在前面的评论中引用:</P><blockquote>
      <p>
    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.
    </p>
     
      <p>
    (...)
    </p>
     
      <p>
    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.
    </p>
    </blockquote>号<P>那么,在一个等幂方法的重复请求之后,"相同"应该是什么呢?不是服务器状态,也不是服务器响应,而是预期的效果。特别是,该方法应该是等幂的"从客户的角度"。现在,我认为这一观点表明,丹·洛答案中的最后一个例子,我不想在这里剽窃,确实表明了一个补丁请求可以是非等幂的(以一种比杰森·霍特格答案中的例子更自然的方式)。</P><P>实际上,让我们通过为第一个客户明确一个可能的意图,使示例稍微更精确一些。假设这个客户通过项目的用户列表来检查他们的电子邮件和邮政编码。他从用户1开始,注意到zip是正确的,但电子邮件是错误的。他决定通过一个完全合法的补丁请求来纠正这个问题,并且只发送</P>[cc]PATCH /users/1
    {"email":"[email protected]"}

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

    如果客户端使用Put请求来更正电子邮件,并将用户1的所有属性连同电子邮件一起发送给服务器,那么即使稍后必须重新发送请求,并且同时修改了用户1,也会达到预期效果,因为第二个Put请求将在ERWRITE自第一个请求以来的所有更改。