我正在为客户管理系统编写一个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)”,“countCustomers()”

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

当前回答

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

其他回答

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

要更新客户123的字段,需要POST到/customer/123。

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

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

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

关于你的更新。

我认为CRUD的概念在API设计方面造成了一些混乱。CRUD是对数据执行基本操作的一般低级概念,HTTP谓词只是请求方法(创建于21年前),可能映射到CRUD操作,也可能不映射到CRUD操作。事实上,请尝试在HTTP 1.0/1.1规范中找到CRUD首字母缩写。

在谷歌云平台API文档中可以找到一个应用实用约定的解释很好的指南。它描述了创建基于资源的API背后的概念,一个强调大量资源而不是操作的API,并包括您正在描述的用例。虽然这只是他们产品的一种惯例设计,但我认为这很有意义。

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

它还说,当一个操作与标准方法(在本例中是List、Get、Create、Update、Delete)不完全匹配时,可以使用“自定义方法”,比如BatchGet,它根据多个对象id输入检索多个对象,或者SendEmail。

你的扩充问题需要补充的东西。我认为您通常可以完美地设计更复杂的业务操作。但你必须放弃方法/过程的思维方式,更多地考虑资源和动词。

邮件发送


POST /customers/123/mails

payload:
{from: x@x.com, subject: "foo", to: y@y.com}

这个资源+ POST的实现将发送邮件。如果有必要,你可以提供类似/customer/123/发件箱这样的东西,然后提供到/customer/mails/{mailId}的资源链接。

客户数

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


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}

对于修改状态,我认为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 -customer到status=ACTIVE的客户。

读操作:

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

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

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

DELETE /customer/123/active

它可以将您的客户带到DELETED/DISABLED状态或相反的状态(ACTIVE/INACTIVE)。

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

它定义了MERGE方法,所以在你的例子中,它是这样的:

MERGE /customer/123

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

只有status属性被更新,其他值被保留。