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


208

我正在为客户管理系统编写RESTful服务,并且试图找到最佳方式来部分更新记录。例如,我希望调用者能够通过GET请求读取完整记录。但是为了进行更新,仅允许记录上的某些操作,例如将状态从“启用”更改为“禁用”。(我有比这更复杂的场景)

出于安全原因,我不希望呼叫者仅使用更新的字段来提交整个记录(这也感觉像是过大了)。

有没有推荐的方法来构造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 

3
为了回答有关“业务逻辑调用”的问题,这里是POSTRoy Fielding自己写的一篇帖子:roy.gbiv.com/untangled/2009/it-is-okay-to-use-post,其基本思想是:如果没有并非最适合您的操作使用的方法(例如GETPUTPOST
rojoca 2010年

这几乎就是我最终要做的。进行REST调用,以使用GET,PUT,DELETE检索和更新已知资源。用于添加新资源的POST和用于业务逻辑调用的带有描述性URL的POST。
magiconair 2011年

无论您决定什么,如果该操作不是GET响应的一部分,那么您就没有RESTful服务。我在这里看不到
MStodd

Answers:


69

您基本上有两个选择:

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

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


好的,POST / 303对我来说很有意义。我在有效的HTTP动词列表中找不到PATCH和MERGE,因此需要进行更多测试。如果我希望系统向客户123发送电子邮件,该如何构造URI?诸如纯RPC方法调用之类的东西根本不会改变对象的状态。RESTful这样做的方式是什么?
magiconair

我不了解电子邮件URI问题。您是否要实现一个可以发布的网关以使其发送电子邮件,或者您正在寻找mailto:customer.123@service.org?
Jan Algermissen

15
除了某些人将HTTP方法与CRUD等同之外,REST和HTTP与CRUD都没有关系。REST是关于通过转移表示来操纵资源状态的。无论您要实现什么目标,都可以通过将表示形式转换为具有适当语义的资源来实现。注意术语“纯方法调用”或“业务逻辑”,因为它们很容易暗示“ HTTP用于传输”。如果您需要发送电子邮件,邮寄到网关的资源,如果你需要合并账户,创建一个新的,其他两个的POST表示等
扬阿尔格尔米森

9
又见谷歌是如何做的:googlecode.blogspot.com/2010/03/...
马吕斯

4
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”:“替换”,“ 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“}]
intotecho

48

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

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

如果只想更新状态,还可以将其放置到/ customer / 123 / status。

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

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


1
@John Saunders POST不一定必须创建可从URI访问的新资源:tools.ietf.org/html/rfc2616#section-9.5
wsorenson 2010年

10
@wsorensen:我知道它不需要生成新的URL,但是仍然认为POST /customer/123应该创建逻辑上在客户123下的显而易见的东西。也许是订单?/customer/123/status假设POST /customers隐式创建了一个status(并假设这是合法的REST),则PUT 似乎更有意义。
约翰·桑德斯

1
@John Saunders:实际上,如果我们想更新位于给定URI上的资源上的字段,则POST比PUT更有意义,并且缺少UPDATE,我相信它通常用于REST服务中。对/ customers进行POST可能会创建一个新客户,对/ customer / 123 / status进行PUT可能会更好地符合规范的字眼,但是就最佳做法而言,我认为没有任何理由不对POST进行/ customer / 123更新字段-简洁,合理,并且不严格违反规范中的任何内容。
wsorenson

8
POST请求不应该是幂等的吗?当然更新条目是幂等的,因此应该改为PUT吗?
Martin Andersson

1
@MartinAndersson POST-REQUESTS并不需要是非幂等。并且如上所述,PUT必须替换整个资源。
Halle Knast

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(原始文档的修改副本)进行部分更新,则很快就会遇到一些问题,您希望缺失值(或者更确切地说是null值)来表示“忽略此属性”或“将该属性设置为空值”-导致被入侵的解决方案无处可寻,最终将产生您自己的补丁格式。

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


请注意,与此同时,针对json-patchxml-patch的RFC 已完成。
botchniaque,2014年

8

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

我可以想到以下两种解决方案:

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

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

2只是带宽优化1。有时,如果资源定义了一些必填字段(例如原始缓冲区),则1是唯一的选择。

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

注释?


2
如果作为单独的问题发布,这将更加有用。
intotecho

6

为了修改状态,我认为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

这样一来,您的客户就可以进入DELETED / DISABLED状态或相反的状态(ACTIVE / INCTIVE)。


您如何获得子资源?
MStodd

我重构了答案,试图使其更加清晰
raspacorp

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


我喜欢POST子资源中字段的逻辑分组方式。
gertas 2014年

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

2

关于您的更新。

我相信CRUD的概念在API设计上引起了一些混乱。CRUD是用于对数据执行基本操作的通用底层概念,HTTP动词只是请求方法(创建于21年前),可以或可以不映射到CRUD操作。实际上,请尝试在HTTP 1.0 / 1.1规范中找到CRUD的缩写。

可以在Google云平台API文档中找到非常实用的指南,该指南采用了实用的约定。它描述了创建基于资源的API的概念,该概念强调操作上的大量资源,并包括您正在描述的用例。尽管这只是他们产品的常规设计,但我认为这很有意义。

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

它还说,当操作不与一个标准的方法精确地映射(ListGetCreateUpdateDelete在这种情况下),可以使用“自定义的方法”,例如BatchGet,以检索基于几个对象ID输入多个对象,或SendEmail


2

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

简而言之,您可以使用application / merge-patch + json MIME媒体类型和主体(仅表示要更改/添加/删除的部分)向目标资源提交HTTP PATCH ,然后遵循以下处理规则。

规则

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

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

  • 合并补丁中的空值具有特殊含义,以指示已删除目标中的现有值。

示例测试案例说明了上述规则(如该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}}}

1

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

它定义了MERGE方法,因此在您的情况下将是这样的:

MERGE /customer/123

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

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


MERGE有效的HTTP动词吗?
约翰·桑德斯

3
看一下PATCH-这是即将成为标准的HTTP,并且执行相同的操作。
Jan Algermissen

@John Saunders是的,这是一种扩展方法。
Max Toro

FYI 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/...
tanguy_k

0

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

如果您的GET响应具有状态转换,则客户端只需要知道如何读取它们,服务器就可以根据需要更改它们。此处使用POST完成更新,但是如果将其更改为PATCH,或者URL更改,则客户端仍然知道如何进行更新:

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

您可能会尽可能列出客户端所需的/可选参数以供您返回。这取决于应用程序。

就业务运营而言,这可能是与客户资源链接的不同资源。如果要向客户发送电子邮件,则该服务可能是您可以过帐的自有资源,因此您可以在客户资源中包括以下操作:

"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/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.