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

mag*_*air 200 rest

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

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

是否有推荐的构建URI的方法?在阅读REST书籍时,RPC样式调用似乎不受欢迎.

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

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>
Run Code Online (Sandbox Code Playgroud)

我该如何更新状态?

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

POST /customer/123/changeStatus
DISABLED

...
Run Code Online (Sandbox Code Playgroud)

更新:增加问题.如何将"业务逻辑调用"纳入REST API?这是否有商定的方式?并非所有方法都是CRUD本质上.有些更复杂,比如' sendEmailToCustomer(123) ',' mergeCustomers(123,456) ',' countCustomers() '

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 
Run Code Online (Sandbox Code Playgroud)

谢谢弗兰克

Jan*_*sen 69

你基本上有两个选择:

  1. 使用PATCH(但请注意,您必须定义自己的媒体类型,以指定将要发生的事情)

  2. 使用POST子资源并返回303 See Other,Location标头指向主资源.303的目的是告诉客户端:"我已经执行了您的POST,结果是其他资源已更新.请参阅位置标头,该资源是哪个." POST/303用于迭代添加资源以构建某些主要资源的状态,它非常适合部分更新.

  • REST和HTTP都没有与CRUD有任何关系,除了一些人将HTTP方法与CRUD等同起来.REST是通过传输表示来操纵资源状态.无论您想要实现什么,都可以通过将表示转移到具有适当语义的资源来实现.谨防术语"纯方法调用"或"业务逻辑",因为它们太容易暗示"HTTP用于传输".如果您需要发送电子邮件,POST到网关资源,如果您需要合并到帐户,创建一个新的和其他两个的POST表示,等等. (15认同)
  • 另请参阅Google的工作方式:http://googlecode.blogspot.com/2010/03/making-apis-faster-introducing-partial.html (9认同)
  • http://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","value":["foo","bar"]},{"op":"替换","路径":"/ a/b/c","值":42}, {"op":"move","from":"/ a/b/c","path":"/ a/b/d"},{"op":"copy","from":"/a/b/d","path":"/ a/b/e"}] (4认同)

wso*_*son 48

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

要更新客户123的字段,请对/ customer/123进行POST.

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

通常,GET请求不应有任何副作用,PUT用于写入/替换整个资源.

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

  • @wsorensen:我知道它不需要产生新的URL,但仍然认为对`/ customer/123`的POST应该创建一个明显的东西,逻辑上在客户123下.也许订单?假设对`/ customers`的POST隐式地创建了一个`status`(并假设它是合法的REST),那么输入`/ customer/123/status`似乎更有意义. (8认同)
  • POST请求不应该是幂等的吗?肯定更新一个条目是幂等的,因此应该是一个PUT? (8认同)

Jør*_*ldt 10

您应该使用补丁部分更新-无论是使用JSON-补丁文件(见http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08http://www.mnot.net/ blog/2012/09/05/patch)或XML补丁框架(参见http://tools.ietf.org/html/rfc5261).在我看来,json-patch最适合您的业务数据.

使用JSON/XML补丁文档的PATCH具有非常严格的前向语义以进行部分更新.如果你开始使用POST,与原文件的修改后的副本,对部分更新你很快就会碰到您想缺失值(或者说,空值)问题表示无论是"忽略此属性"或"此属性设置为空值" - 这导致了一个被黑客入侵的解决方案的兔子洞,最终将导致你自己的补丁格式.

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


小智 8

我遇到了类似的问题.当您只想更新单个字段时,子资源上的PUT似乎有效.但是,有时您需要更新一堆内容:考虑表示资源的Web表单,并选择更改某些条目.用户提交表单不应导致多个PUT.

以下是我能想到的两种解决方案:

  1. 用整个资源做一个PUT.在服务器端,定义具有整个资源的PUT忽略所有未更改的值的语义.

  2. 使用部分资源执行PUT.在服务器端,将其语义定义为合并.

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

这两种方法的问题在于如何清除字段.您将不得不定义一个特殊的空值(特别是对于原型缓冲区,因为没有为原型缓冲区定义空值),这将导致字段清除.

评论?

  • 如果作为单独的问题发布,这将更有用. (2认同)

man*_*ana 5

要添加到增强问题的内容.我认为您通常可以完美地设计更复杂的业务操作.但是你必须放弃思考的方法/程序风格,并在资源和动词中思考更多.

邮件发送


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: { ...} ....}


ras*_*orp 5

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

例:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}
Run Code Online (Sandbox Code Playgroud)

POST服务应返回ID为:

{
    id:123,
    ...  // the other fields here
}
Run Code Online (Sandbox Code Playgroud)

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

GET /customer/123/active
Run Code Online (Sandbox Code Playgroud)

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

对于PUT操作,无需提供Json实体,它将仅更新状态

PUT /customer/123/inactive  <-- Deactivating an existing customer
Run Code Online (Sandbox Code Playgroud)

提供实体将使您可以更新客户的内容并同时更新状态。

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}
Run Code Online (Sandbox Code Playgroud)

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

读取操作:

GET /customer/123/active 
GET /customer/123/inactive
Run Code Online (Sandbox Code Playgroud)

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

DELETE操作很有趣,因为语义可能会造成混淆。但是,您可以选择不为该概念性资源发布该操作,或者根据您的业务逻辑使用该操作。

DELETE /customer/123/active
Run Code Online (Sandbox Code Playgroud)

这样一来,您的客户就可以进入已删除/已禁用状态或处于相反状态(活动/无效)。


Voi*_*icu 5

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

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

规则

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

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

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

说明上述规则的示例测试用例(如该 RFC的附录所示):

 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}}}
Run Code Online (Sandbox Code Playgroud)