REST API-在单个请求中批量创建或更新


92

让我们假设有两种资源,Binder并且Doc具有关联关系,这意味着DocBinder各自独立。Doc可能属于或可能不属于Binder并且Binder可能为空。

如果我想设计一个允许用户发送Docs 集合的REST API ,请在单个请求中,如下所示:

{
  "docs": [
    {"doc_number": 1, "binder": 1}, 
    {"doc_number": 5, "binder": 8},
    {"doc_number": 6, "binder": 3}
  ]
}

对于中的每个文档docs

  • 如果doc存在,则将其分配给Binder
  • 如果doc不存在,请创建它,然后分配它

我对如何实现这一点感到非常困惑:

  • 使用哪种HTTP方法?
  • 必须返回什么响应代码?
  • 这甚至符合REST的资格吗?
  • URI看起来如何?/binders/docs
  • 处理批量请求,如果有几个项目出现错误而另一个通过则怎么办。必须返回什么响应代码?批量操作是否应该是原子操作?

Answers:


58

我认为您可以使用POST或PATCH方法来处理此问题,因为它们通常是为此设计的。

  • 使用POST方法通常用于在列表资源上添加元素时,但是您也可以支持此方法的多个操作。请参见以下答案:如何更新REST资源集合。您还可以支持输入的不同表示形式(如果它们对应于数组或单个元素)。

    在这种情况下,无需定义格式来描述更新。

  • PATCH由于相应的请求对应于部分更新,因此使用方法也是合适的。根据RFC5789(http://tools.ietf.org/html/rfc5789):

    几个扩展超文本传输​​协议(HTTP)的应用程序需要一项功能来进行部分资源修改。现有的HTTP PUT方法仅允许完全替换文档。该提议添加了新的HTTP方法PATCH,以修改现有的HTTP资源。

    在这种情况下,您必须定义格式来描述部分更新。

我认为,在这种情况下,POST并且PATCH是非常相似的,因为你并不真的需要介绍操作的每个元素做。我要说的是,这取决于要发送的表示形式。

的情况PUT不太清楚。实际上,使用方法时PUT,应提供整个列表。实际上,请求中提供的表示将代替列表资源。

关于资源路径,您可以有两个选择。

  • 将资源路径用于文档列表

在这种情况下,您需要在请求中提供的表示形式中明确提供文档链接和活页夹。

这是为此的示例路线/docs

这种方法的内容可能是针对以下方法POST

[
    { "doc_number": 1, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 2, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 3, "binder": 5, (other fields in the case of creation) },
    (...)
]
  • 使用活页夹元素的子资源路径

另外,您还可以考虑利用子路由来描述文档和活页夹之间的链接。现在无需在请求内容中指定有关文档和活页夹之间关联的提示。

这是为此的示例路线/binder/{binderId}/docs。在这种情况下,发送带有方法的文档列表,POST或者在创建文档后将PATCH文档附加到具有标识符的资料夹(binderId如果不存在)。

这种方法的内容可能是针对以下方法POST

[
    { "doc_number": 1, (other fields in the case of creation) },
    { "doc_number": 2, (other fields in the case of creation) },
    { "doc_number": 3, (other fields in the case of creation) },
    (...)
]

关于响应,由您决定响应的级别和返回的错误。我看到两个级别:状态级别(全局级别)和有效负载级别(更薄的级别)。您还可以定义与您的请求相对应的所有插入/更新是否必须是原子的。

  • 原子

在这种情况下,您可以利用HTTP状态。如果一切顺利,您将获得一个状态200。如果不是,400则为其他状态,例如提供的数据不正确(例如,绑定器ID无效)或其他状态。

  • 非原子

在这种情况下,200将返回状态,并由响应表示来描述已完成的操作以及错误最终在何处发生。ElasticSearch在其REST API中具有一个端点,用于批量更新。这可以为您提供一些有关此级别的想法:http : //www.elasticsearch.org/guide/en/elasticsearch/guide/current/bulk.html

  • 异步

您还可以实现异步处理来处理提供的数据。在这种情况下,HTTP状态返回将为202。客户端需要提取其他资源以查看会发生什么。

在结束之前,我还想注意到OData规范通过名为Navigation links的功能解决了有关实体之间关系的问题。也许你可以看看这个;-)

以下链接也可以为您提供帮助:https : //templth.wordpress.com/2014/12/15/designing-a-web-api/

希望对您有帮助,蒂埃里


我一直在追问。我选择了没有嵌套子资源的平坦路线。要获取所有文档,我调用GET /docs并检索特定活页夹中的所有文档GET /docs?binder_id=x。要删除的资源的一个子集我会打电话DELETE /docs?binder_id=x或者我应该叫DELETE /docs一个{"binder_id": x}请求体?您是否会使用PATCH /docs?binder_id=x批量更新或正PATCH /docs对通过?
安迪·富斯尼亚克

34

您可能需要使用POST或PATCH,因为更新和创建多个资源的单个请求不太可能是幂等的。

这样做PATCH /docs绝对是一个有效的选择。您可能会发现针对特定情况使用标准补丁程序格式有些棘手。对此不确定。

您可以使用200。也可以使用207-多状态

这可以通过RESTful方式完成。我认为,关键是要拥有一些旨在接受一组要更新/创建的文档的资源。

如果您使用PATCH方法,我认为您的操作应该是原子的。也就是说,我不会使用207状态代码,然后在响应正文中报告成功和失败。如果使用POST操作,则207方法可行。您将必须设计自己的响应主体以传达哪些操作成功和哪些失败。我不知道一个标准化的。


非常感谢。通过This can be done in a RESTful way你的意思是更新和创建必须单独做了什么?
山姆R.

1
@norbertpy对资源执行某种写操作可能导致从单个请求中更新和创建其他资源。REST对此没有任何问题。我选择这句话是因为某些框架通过将HTTP请求序列化为多部分文档,然后批量发送序列化的HTTP请求来实现批量操作。我认为这种方法违反了资源标识REST约束。
Darrel Miller

19

PUT ING

PUT /binders/{id}/docs 创建或更新单个文档并将其与活页夹关联

例如:

PUT /binders/1/docs HTTP/1.1
{
  "docNumber" : 1
}

PATCH ING

PATCH /docs 创建不存在的文档,并将其与活页夹相关联

例如:

PATCH /docs HTTP/1.1
[
    { "op" : "add", "path" : "/binder/1/docs", "value" : { "doc_number" : 1 } },
    { "op" : "add", "path" : "/binder/8/docs", "value" : { "doc_number" : 8 } },
    { "op" : "add", "path" : "/binder/3/docs", "value" : { "doc_number" : 6 } }
] 

稍后我将提供其他见解,但是如果您愿意的话,同时查看RFC 5789RFC 6902和William Durand的Please。不要像白痴博客条目那样打补丁


2
有时客户端需要批量操作,并且它不希望资源是否存在。正如我在问题中所说的,客户希望发送一堆docs并将其与关联binders。客户希望创建不存在的活页夹,如果存在则进行关联。在一个大容量请求中。
山姆R.

12

在我工作的一个项目中,我们通过实现称为“批处理”请求的方法解决了此问题。我们/batch以以下格式定义了接受json 的路径:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 5,
         binder: 8
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      }
   },
]

响应的状态码为207(多状态),如下所示:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
      status: 200
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         error: {
            msg: 'A document with doc_number 5 already exists'
            ...
         }
      },
      status: 409
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      },
      status: 200
   },
]

您也可以在此结构中添加对标头的支持。我们实现了一些有用的东西,它是在批量请求之间使用的变量,这意味着我们可以将一个请求的响应用作另一个请求的输入。

Facebook和Google具有类似的实现方式:
https : //developers.google.com/gmail/api/guides/batch
https://developers.facebook.com/docs/graph-api/making-multiple-requests

当您想通过相同的调用创建或更新资源时,根据情况,我将使用POST或PUT。如果文档已经存在,您是否希望整个文档为:

  1. 替换为您发送的文档(即,请求中缺少的属性将被删除并且已经被覆盖)?
  2. 与您发送的文档合并(即,请求中缺少的属性将不会被删除,并且现有的属性将被覆盖)?

如果您想要替代方案1中的行为,则应使用POST,如果您想要替代方案2中的行为,则应使用PUT。

http://restcookbook.com/HTTP%20Methods/put-vs-post/

正如人们已经建议的那样,您也可以选择PATCH,但我宁愿保持API的简单性,不要在不需要时使用多余的动词。


5
像这样的概念验证答案以及Google和Facebook链接。但是不同意关于POST或PUT的结尾部分。在提到的两种情况下,第一个应该是PUT,第二个应该是PATCH。
RayLuo

@RayLuo,您能解释一下为什么除了POST和PUT之外还需要PATCH吗?
David Berg

2
因为那是PATCH的发明目的。您可以阅读此定义,并查看PUT和PATCH如何匹配您的2个项目符号。
RayLuo

@DavidBerg,看来Google首选了另一种处理批处理请求的方法,即将每个子请求的标头和主体分离到主请求的相应部分,并带有一个边界--batch_xxxx。Google和Facebook的解决方案之间有一些重要的区别吗?另外,关于“将一个请求的响应用作另一个请求的输入”,这听起来很有趣,您愿意共享更多详细信息吗?还是应该使用哪种方案?
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.