RESTful 服务中部分更新的最佳实践

Posted

技术标签:

【中文标题】RESTful 服务中部分更新的最佳实践【英文标题】:Best practice for partial updates in a RESTful service 【发布时间】:2011-01-27 10:49:20 【问题描述】:

我正在为客户管理系统编写一个 RESTful 服务,并试图找到部分更新记录的最佳实践。例如,我希望调用者能够通过 GET 请求读取完整记录。但是为了更新它,只允许对记录进行某些操作,例如将状态从 ENABLED 更改为 DISABLED。 (我有比这更复杂的场景)

出于安全原因,我不希望调用者仅使用更新的字段提交整个记录(这也感觉有点矫枉过正)。

是否有推荐的构建 URI 的方法?在阅读 REST 书籍时,似乎不赞成 RPC 风格的调用。

如果以下调用返回 id 为 123 的客户的完整客户记录

GET /customer/123
<customer>
    lots of attributes
    <status>ENABLED</status>
    even more attributes
</customer>

我应该如何更新状态?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

更新:增加问题。如何将“业务逻辑调用”合并到 REST api 中?有没有商定的方式来做到这一点?并非所有方法本质上都是 CRUD。有些更复杂,例如 'sendEmailToCustomer(123)'、'mergeCustomers(123, 456)'、'counterCustomers()' p>

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

【问题讨论】:

要回答您关于“业务逻辑调用”的问题,这里有一篇来自 Roy Fielding 本人的关于 POST 的帖子:roy.gbiv.com/untangled/2009/it-is-okay-to-use-post 其中的基本思想是:如果没有方法(例如GETPUT) 非常适合您的操作使用 POST 这几乎就是我最终所做的。使用 GET、PUT、DELETE 进行 REST 调用以检索和更新已知资源。 POST 用于添加新资源,POST 带有一些用于业务逻辑调用的描述性 URL。 无论您决定什么,如果该操作不是 GET 响应的一部分,那么您就没有 RESTful 服务。我这里没看到 【参考方案1】:

你基本上有两种选择:

    使用PATCH(但请注意,您必须定义自己的媒体类型来指定确切发生的情况)

    POST 用于子资源并返回 303 See Other 并带有指向主资源的 Location 标头。 303 的目的是告诉客户端:“我已经执行了您的 POST,结果是其他一些资源已更新。请参阅 Location 标头以了解该资源。” POST/303 旨在对资源进行迭代添加以建立一些主要资源的状态,它非常适合部分更新。

【讨论】:

好的,POST/303 对我来说很有意义。 PATCH 和 MERGE 我在有效 HTTP 动词列表中找不到,因此需要更多测试。如果我希望系统向客户 123 发送电子邮件,我将如何构建 URI?类似于纯粹的 RPC 方法调用,它根本不会改变对象的状态。这样做的 RESTful 方式是什么? 我不明白电子邮件 URI 问题。您想实现一个可以 POST 到的网关,让它发送电子邮件,还是在寻找 mailto:customer.123@service.org? 除了一些人将 HTTP 方法等同于 CRUD 之外,REST 和 HTTP 都与 CRUD 无关。 REST 是关于通过传输表示来操纵资源状态。无论您想实现什么,您都可以通过将表示转移到具有适当语义的资源来实现。注意术语“纯方法调用”或“业务逻辑”,因为它们太容易暗示“HTTP 用于传输”。如果您需要发送电子邮件,POST 到网关资源,如果您需要合并到帐户,创建一个新帐户并 POST 表示其他两个帐户,等等。 看看谷歌是怎么做的:googlecode.blogspot.com/2010/03/… williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [ "op": "test", "path": "/a/b/c", "value": "foo" , "op": "删除”,“路径”:“/a/b/c”,“op”:“添加”,“路径”:“/a/b/c”,“值”:[“foo”,“bar " ] , "op": "replace", "path": "/a/b/c", "value": 42 , "op": "move", "from": "/a/ b/c", "path": "/a/b/d" , "op": "copy", "from": "/a/b/d", "path": "/a/b /e" ]【参考方案2】:

您应该使用 POST 进行部分更新。

要更新客户 123 的字段,请向 /customer/123 发送 POST。

如果您只想更新状态,也可以 PUT 到 /customer/123/status。

一般来说,GET 请求不应该有任何副作用,而 PUT 是用于写入/替换整个资源的。

这直接来自 HTTP,如下所示:http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods

【讨论】:

@John Saunders POST 不必创建可从 URI 访问的新资源:tools.ietf.org/html/rfc2616#section-9.5 @wsorensen:我知道它不需要产生新的 URL,但仍然认为 POST 到 /customer/123 应该创建在逻辑上位于客户 123 下的明显内容。也许是订单? PUT 到 /customer/123/status 似乎更有意义,假设到 /customers 的 POST 隐式创建了一个 status(并假设这是合法的 REST)。 @John Saunders:实际上,如果我们想更新位于给定 URI 的资源上的字段,POST 比 PUT 更有意义,并且缺少 UPDATE,我相信它经常用于休息服务。 POST 到 /customers 可能会创建一个新客户,而 PUT 到 /customer/123/status 可能更符合规范中的内容,但至于最佳实践,我认为没有任何理由不 POST 到 / customer/123 来更新字段 - 它简洁、有意义,并且不严格违反规范中的任何内容。 POST请求不应该是幂等的吗?确定更新条目是幂等的,因此应该改为 PUT? @MartinAndersson POST-请求不需要需要是非幂等的。如前所述,PUT 必须替换整个资源。【参考方案3】:

您应该使用 PATCH 进行部分更新 - 使用 json-patch 文档(参见 https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-patch-08 或 http://www.mnot.net/blog/2012/09/05/patch)或 XML 补丁框架(参见 https://www.rfc-editor.org/rfc/rfc5261)。不过在我看来,json-patch 最适合您的业务数据类型。

带有 JSON/XML 补丁文档的 PATCH 对于部分更新具有非常严格的前向语义。如果您开始使用 POST 并使用原始文档的修改副本进行部分更新,您很快就会遇到您希望缺失值(或者更确切地说是空值)表示“忽略此属性”或“将此属性设置为空值”——这会导致一个被破解的解决方案的兔子洞,最终会产生你自己的补丁格式。

您可以在这里找到更深入的答案:http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html。

【讨论】:

请注意,同时json-patch 和xml-patch 的RFC 已经完成。【参考方案4】:

我遇到了类似的问题。当您只想更新单个字段时,PUT 似乎可以工作。但是,有时您想要更新一堆东西:考虑一个代表资源的 Web 表单,可以选择更改某些条目。用户提交的表单不应导致多个 PUT。

这是我能想到的两个解决方案:

    对整个资源执行 PUT。在服务器端,定义包含整个资源的 PUT 忽略所有未更改的值的语义。

    使用部分资源执行 PUT。在服务器端,将 this 的语义定义为合并。

2 只是 1 的带宽优化。如果资源定义某些字段是必需字段(想想 proto 缓冲区),有时 1 是唯一的选择。

这两种方法的问题是如何清除字段。您必须定义一个特殊的 null 值(特别是对于 proto 缓冲区,因为未为 proto 缓冲区定义 null 值),这将导致清除该字段。

评论?

【讨论】:

如果作为单独的问题发布会更有用。【参考方案5】:

对于修改状态,我认为 RESTful 方法是使用描述资源状态的逻辑子资源。当您的状态集减少时,此 IMO 非常有用且干净。它使您的 API 更具表现力,而不会强制您的客户资源执行现有操作。

例子:

POST /customer/active  <-- Providing entity in the body a new customer

  ...  // attributes here except status

POST 服务应返回带有 id 的新创建客户:


    id:123,
    ...  // the other fields here

所创建资源的 GET 将使用资源位置:

GET /customer/123/active

GET /customer/123/inactive 应该返回 404

对于PUT操作,不提供Json实体只会更新状态

PUT /customer/123/inactive  <-- Deactivating an existing customer

提供实体将允许您更新客户的内容并同时更新状态。

PUT /customer/123/inactive

    ...  // entity fields here except id and status

您正在为您的客户资源创建概念性子资源。这也符合 Roy Fielding 对资源的定义:“......资源是对一组实体的概念映射,而不是在任何特定时间点对应于映射的实体......”在这种情况下,概念映射是活动客户到状态=ACTIVE 的客户。

读操作:

GET /customer/123/active 
GET /customer/123/inactive

如果您在另一个调用必须返回状态 404 之后立即进行这些调用,则成功输出可能不包含该状态,因为它是隐式的。当然,您仍然可以使用 GET /customer/123?status=ACTIVE|INACTIVE 直接查询客户资源。

DELETE 操作很有趣,因为它的语义可能令人困惑。但是您可以选择不为这个概念资源发布该操作,或者根据您的业务逻辑使用它。

DELETE /customer/123/active

这可以使您的客户进入已删除/已禁用状态或相反状态(活动/非活动)。

【讨论】:

如何获取子资源? 我重构了答案以使其更清晰【参考方案6】:

RFC 7396:JSON Merge Patch(在问题发布四年后发布)描述了 PATCH 在格式和处理规则方面的最佳实践。

简而言之,您将 HTTP PATCH 提交到具有 application/merge-patch+json MIME 媒体类型的目标资源和仅代表您想要更改/添加的部分的正文/removed 然后按照以下处理规则。

规则

如果提供的合并补丁包含未出现在目标中的成员,则会添加这些成员。

如果目标确实包含该成员,则替换该值。

合并补丁中的空值被赋予特殊含义,以指示删除目标中的现有值。

说明上述规则的示例测试用例(见该 RFC 的 appendix):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
"a":"b"       "a":"c"       "a":"c"

"a":"b"       "b":"c"       "a":"b",
                                 "b":"c"
"a":"b"       "a":null      

"a":"b",       "a":null      "b":"c"
"b":"c"

"a":["b"]     "a":"c"       "a":"c"

"a":"c"       "a":["b"]     "a":["b"]

"a":          "a":          "a": 
  "b": "c"       "b": "d",       "b": "d"
                 "c": null      
                               

"a": [         "a": [1]      "a": [1]
  "b":"c"
 ]


["a","b"]       ["c","d"]       ["c","d"]

"a":"b"       ["c"]           ["c"]

"a":"foo"     null            null

"a":"foo"     "bar"           "bar"

"e":null      "a":1         "e":null,
                                 "a":1

[1,2]           "a":"b",       "a":"b"
                 "c":null

              "a":            "a":
                 "bb":           "bb":
                  "ccc":          
                   null

【讨论】:

【参考方案7】:

要添加到您的扩充问题中的内容。我认为您通常可以完美地设计更复杂的业务操作。但是你必须放弃方法/程序的思维方式,更多地考虑资源和动词。

邮件发送

POST /customers/123/mails payload: from: x@x.com, subject: "foo", to: y@y.com

这个资源 + POST 的实现然后会发送邮件。如有必要,您可以提供 /customer/123/outbox 之类的内容,然后提供指向 /customer/mails/mailId 的资源链接。

客户数量

您可以像处理搜索资源一样处理它(包括带有分页和 num-found 信息的搜索元数据,它可以为您提供客户数量)。

GET /customers response payload: numFound: 1234, paging: self:..., next:..., previous:... customer: ... ....

【讨论】:

我喜欢POST子资源中字段的逻辑分组方式。【参考方案8】:

使用 PUT 更新不完整/部分资源。

您可以接受 jObject 作为参数并解析其值以更新资源。

下面是Java 函数,您可以用作参考:

public IHttpActionResult Put(int id, JObject partialObject) 
    Dictionary < string, string > dictionaryObject = new Dictionary < string, string > ();

    foreach(JProperty property in json.Properties()) 
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);

【讨论】:

【参考方案9】:

关于您的更新。

我认为 CRUD 的概念在 API 设计方面引起了一些混乱。 CRUD 是对数据执行基本操作的一般低级概念,HTTP 动词只是请求方法 (created 21 years ago),可能映射到也可能不映射到 CRUD 操作。实际上,请尝试在 HTTP 1.0/1.1 规范中查找 CRUD 首字母缩写词的存在。

可以在Google cloud platform API documentation 中找到适用于实用约定的很好解释的指南。它描述了创建基于资源的 API 背后的概念,该 API 强调大量资源而不是操作,并包括您所描述的用例。虽然只是他们产品的约定俗成的设计,但我认为这很有意义。

这里的基本概念(并且会产生很多混淆)是“方法”和 HTTP 动词之间的映射。一件事是定义您的 API 将对哪些类型的资源执行哪些“操作”(方法)(例如,获取客户列表或发送电子邮件),另一件事是 HTTP 动词。必须定义您计划使用的方法和动词,以及它们之间的映射

它还说,当操作与标准方法(ListGetCreateUpdateDelete 在这种情况下)不完全映射时,可以使用“自定义方法",如BatchGet,它根据多个对象ID输入检索多个对象,或SendEmail

【讨论】:

【参考方案10】:

查看http://www.odata.org/

它定义了 MERGE 方法,所以在你的情况下它会是这样的:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

仅更新status 属性,保留其他值。

【讨论】:

MERGE 是一个有效的 HTTP 动词吗? 看看 PATCH - 即将成为标准 HTTP 并且做同样的事情。 @John Saunders 是的,这是一种扩展方法。 仅供参考 MERGE 已从 OData v4 中删除。 MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE.见docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…【参考方案11】:

没关系。就 REST 而言,您不能执行 GET,因为它不可缓存,但是使用 POST 或 PATCH 或 PUT 或其他什么都没有关系,而且 URL 看起来如何也没有关系。如果您正在使用 REST,重要的是当您从服务器获取资源的表示时,该表示能够为客户端状态转换提供选项。

如果您的 GET 响应有状态转换,客户端只需要知道如何读取它们,服务器可以在需要时更改它们。这里使用 POST 进行更新,但如果更改为 PATCH,或者 URL 更改,客户端仍然知道如何进行更新:


  "customer" :
  
  ,
  "operations":
  [
    "update" : 
    
      "method": "POST",
      "href": "https://server/customer/123/"
    ]

您可以列出客户所需/可选参数以回馈给您。这取决于应用程序。

就业务运营而言,这可能是与客户资源相关联的不同资源。如果您想向客户发送电子邮件,也许该服务是您可以 POST 到的自己的资源,因此您可以在客户资源中包含以下操作:

"email":

  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"

这些是一些不错的视频,以及演示者的 REST 架构示例。 Stormpath 只使用 GET/POST/DELETE,这很好,因为 REST 与您使用的操作或 URL 的外观无关(GET 应该是可缓存的除外):

https://www.youtube.com/watch?v=pspy1H6A3FM,https://www.youtube.com/watch?v=5WXYw4J4QOU,http://docs.stormpath.com/rest/quickstart/

【讨论】:

以上是关于RESTful 服务中部分更新的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

我们必须要知道的RESTful服务最佳实践

在 RESTful API 中检查用户权限的最佳实践

Restful最佳实践

Restful三分钟彻底了解Restful最佳实践

Restful三分钟彻底了解Restful最佳实践

编写 Node.js RESTful API 的 10 个最佳实践