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

时间:2010-03-14 18:57:19

标签: rest

我正在为客户管理系统编写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 

由于 弗兰克

11 个答案:

答案 0 :(得分:67)

您基本上有两个选择:

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

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

答案 1 :(得分:47)

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

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

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

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

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

答案 2 :(得分:10)

您应该使用PATCH进行部分更新 - 使用json-patch文档(请参阅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

答案 3 :(得分:6)

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

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

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

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

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

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

    评论

答案 4 :(得分: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: { ...} ....}

答案 5 :(得分:4)

为了修改状态,我认为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对资源的定义一致:&#34; ...资源是对一组实体的概念映射,而不是与任何特定时间点的映射相对应的实体。 ..&#34;在这种情况下,概念映射是活动的 - 客户到客户,状态= 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)。

答案 6 :(得分:3)

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

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

以下是您可以用作参考的功能:

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

答案 7 :(得分:1)

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

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

MERGE /customer/123

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

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

答案 8 :(得分:1)

关于您的更新。

我相信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谓词。必须有两者的定义,您计划使用的方法和动词以及它们之间的映射

它还表示,当某个操作未完全使用标准方法(ListGetCreateUpdateDelete进行映射时在这种情况下),可以使用“自定义方法”,如BatchGet,它可以根据多个对象ID输入或SendEmail检索多个对象。

答案 9 :(得分:0)

没关系。在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/

答案 10 :(得分:0)

RFC 7396 JSON合并补丁程序(在问题发布四年后发布)从格式和处理规则方面描述了PATCH的最佳做法。

简而言之,您可以使用 application / merge-patch + json MIME媒体类型和仅表示要更改/添加的部分的主体来向目标资源提交HTTP PATCH。 / 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}}}