我正在为客户管理系统编写 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
POST
的帖子:roy.gbiv.com/untangled/2009/it-is-okay-to-use-post 其中的基本思想是:如果没有方法(例如 GET
或 PUT
)非常适合您的操作使用 POST
。
你基本上有两个选择:
使用 PATCH (但请注意,您必须定义自己的媒体类型来指定确切发生的情况)使用 POST 到子资源并返回 303 See Other 并带有指向主资源的 Location 标头。 303 的目的是告诉客户端:“我已经执行了您的 POST,结果是其他一些资源已更新。请参阅 Location 标头以了解该资源。” POST/303 旨在对资源进行迭代添加以建立一些主要资源的状态,它非常适合部分更新。
您应该使用 POST 进行部分更新。
要更新客户 123 的字段,请对 /customer/123 进行 POST。
如果您只想更新状态,也可以 PUT 到 /customer/123/status。
一般来说,GET 请求不应该有任何副作用,而 PUT 用于写入/替换整个资源。
这直接来自 HTTP,如下所示:http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods
/customer/123
应该创建在逻辑上位于客户 123 下的明显事物。也许是订单? PUT 到 /customer/123/status
似乎更有意义,假设到 /customers
的 POST 隐式创建了一个 status
(并假设这是合法的 REST)。
POST
-请求不需要 是非幂等的。如前所述,PUT
必须替换整个资源。
您应该使用 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。
我遇到了类似的问题。当您只想更新单个字段时,PUT 似乎可以工作。但是,有时您想更新一堆东西:考虑一个代表资源的 Web 表单,可以选择更改某些条目。用户提交的表单不应导致多个 PUT。
这是我能想到的两个解决方案:
对整个资源执行 PUT。在服务器端,定义包含整个资源的 PUT 忽略所有未更改的值的语义。使用部分资源执行 PUT。在服务器端,将 this 的语义定义为合并。
2 只是 1 的带宽优化。如果资源定义某些字段是必需字段(想想 proto 缓冲区),有时 1 是唯一的选择。
这两种方法的问题是如何清除字段。您必须定义一个特殊的空值(特别是对于原型缓冲区,因为未为原型缓冲区定义空值),这将导致清除该字段。
注释?
RFC 7396:JSON Merge Patch(在问题发布四年后发布)描述了 PATCH 在格式和处理规则方面的最佳做法。
简而言之,您将 HTTP PATCH 提交到具有 application/merge-patch+json MIME 媒体类型的目标资源和仅表示您想要更改/添加/删除的部分的主体,然后遵循以下处理规则。
规则:
如果提供的合并补丁包含未出现在目标中的成员,则会添加这些成员。如果目标确实包含该成员,则替换该值。合并补丁中的空值被赋予特殊含义,以指示删除目标中的现有值。
说明上述规则的示例测试用例(如该 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}}}
对于修改状态,我认为 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
那个可以使您的客户进入删除/禁用状态或相反状态(活动/非活动)。
要添加到您的增强问题中的内容。我认为您通常可以完美地设计更复杂的业务操作。但是你必须放弃方法/程序的思维方式,更多地考虑资源和动词。
邮件发送
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: { ...} ....}
使用 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);
}
关于您的更新。
我相信 CRUD 的概念在 API 设计上造成了一些混乱。 CRUD 是对数据执行的基本操作的一般低级概念,HTTP 动词只是可能映射到也可能不映射到 CRUD 操作的请求方法 (created 21 years ago)。事实上,尝试在 HTTP 1.0/1.1 规范中找到 CRUD 首字母缩写词的存在。
可以在 Google cloud platform API documentation 中找到一个解释得很清楚的应用实用约定的指南。它描述了创建基于资源的 API 背后的概念,该 API 强调大量资源而不是操作,并包括您所描述的用例。虽然只是他们产品的常规设计,但我认为这很有意义。
这里的基本概念(并且会产生很多混淆)是“方法”和 HTTP 动词之间的映射。一件事是定义您的 API 将对哪些类型的资源执行哪些“操作”(方法)(例如,获取客户列表或发送电子邮件),另一件事是 HTTP 动词。必须有你计划使用的方法和动词的定义以及它们之间的映射。
它还说,当操作与标准方法(在这种情况下为 List
、Get
、Create
、Update
、Delete
)不完全映射时,可以使用“自定义方法”,例如BatchGet
,它根据多个对象 ID 输入检索多个对象,或 SendEmail
。
它定义了 MERGE 方法,因此在您的情况下,它将是这样的:
MERGE /customer/123
<customer>
<status>DISABLED</status>
</customer>
仅更新 status
属性并保留其他值。
MERGE
是一个有效的 HTTP 动词吗?
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/…
没关系。就 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/
不定期副业成功案例分享