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

122

假设有两个资源BinderDoc,它们之间存在关联关系,意味着DocBinder都可以独立存在。 Doc可能属于Binder,也可能不属于Binder,而Binder可能为空。

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

{
  "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
  • 如何处理批量请求,如果一些项引发错误而其他项通过了。 必须返回什么响应代码? 批量操作应该是原子性的吗?

不幸的是,如此讨论,在批处理方面HTTP并不理想。HTTP本质上只是一种远程文档管理协议,重点是单个文档。我们可能会向服务器发送单个“文档”,并将其视为影响多个文档,但在该过程中,您将有效地绕过任何(中介)缓存,因为您没有分别针对这些资源。 - Roman Vottner
4个回答

77

我认为您可以使用POSTPATCH方法来处理此操作,因为它们通常被设计用于这种情况。

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

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

  • 使用PATCH方法也是适合的,因为对应的请求相当于进行部分更新。根据RFC5789 (https://www.rfc-editor.org/rfc/rfc5789):

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

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

我认为在这种情况下,POSTPATCH非常相似,因为您不需要为每个元素描述要执行的操作。我会说这取决于要发送的表示形式的格式。

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。在这种情况下,使用 POSTPATCH 方法发送文档列表将在创建文档并将其附加到标识符为 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。否则,如果提供的数据不正确(例如捆绑器ID无效)或其他情况,则会返回另一个状态,例如400

  • 非原子性

在这种情况下,将返回状态200,响应表示描述了完成的操作以及错误发生的地方。ElasticSearch在其REST API中有一个端点用于批量更新,这可能会给您在此级别上提供一些想法:http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/bulk.html

  • 异步

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

最后,我还想提醒一下,OData规范涉及实体之间关系的问题,使用名为导航链接的功能。也许你可以看一下这个 ;-)

以下链接也可能对你有所帮助:https://templth.wordpress.com/2014/12/15/designing-a-web-api/

希望能帮到你, Thierry


我有一个问题。我选择了没有嵌套子资源的平路由。要获取所有文档,我调用 GET /docs 并检索特定装订器中的所有文档,GET /docs?binder_id=x。要删除资源的子集,我应该调用 DELETE /docs?binder_id=x 还是在请求正文中调用 {"binder_id": x} 的情况下调用 DELETE /docs?您是否会使用 PATCH /docs?binder_id=x 进行批量更新,还是只需使用 PATCH /docs 并传递成对的数据? - Andy Fusniak

44

您可能需要使用POST或PATCH,因为更新和创建多个资源的单个请求很难具有幂等性。

执行PATCH /docs绝对是一个有效的选择。对于特定情况,您可能会发现使用标准的补丁格式有些棘手。不确定。

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

这可以以RESTful方式完成。在我看来,关键是设计某些资源以接受要更新/创建的一组文档。

如果您使用PATCH方法,我认为您的操作应该是原子的。即不要使用207状态码,然后在响应体中报告成功和失败。如果您使用POST操作,则207方法是可行的。您将必须为通信哪些操作成功和哪些失败而设计自己的响应体。我不知道是否有标准化的方法。


非常感谢。您所说的“这可以以RESTful方式完成”,是指必须分别进行更新和创建吗? - Sam R.
1
@norbertpy 在资源上执行某种写操作可能会导致从单个请求中更新和创建其他资源。REST 对此没有问题。我的措辞选择是因为一些框架通过将 HTTP 请求序列化为多部分文档,然后将序列化的 HTTP 请求作为批处理发送来实现批量操作。我认为这种方法违反了资源标识 REST 约束。 - Darrel Miller
207是WebDAV定义的状态码。通用的HTTP客户端可能不会意识到该状态码,遗憾的是。此外,由于缓存是REST拥有的少数约束之一,任何提出的“解决方法”都将完全绕过缓存。不幸的是,HTTP在其核心设计上是围绕单个文档交换而不是批处理处理的。 - Roman Vottner

22

PUT 操作

PUT /binders/{id}/docs 创建或更新并将单个文档关联到文件夹

例如:

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

PATCH更新

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. Don't Patch Like an Idiot博客文章。


2
有时客户需要批量操作,而且不想关心资源是否存在。正如我在问题中所说的,客户希望发送一堆“文档”并将它们与“绑定器”相关联。如果这些绑定器不存在,客户希望创建它们,并在它们存在时进行关联。所有这些操作都在一个单独的批量请求中完成。 - Sam R.

19
在我参与的一个项目中,我们通过实现所谓的“批处理”请求来解决了这个问题。我们定义了一个路径 /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 PUT,如果您希望获得备选方案2的行为,则应使用PUT PATCH。

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


7
同意这个概念验证以及Google和Facebook的链接。但是不同意最后提到的POST或PUT的部分。在这个答案提到的两种情况中,第一个应该是PUT,第二个应该是PATCH。 - RayLuo
@RayLuo,你能解释一下为什么我们需要PATCH,除了POST和PUT之外吗? - David Berg
3
那是 PATCH 的设计目的。你可以阅读此定义并查看 PUT 和 PATCH 如何符合你提出的两个要点。 - RayLuo
@DavidBerg,看起来Google已经采用了另一种处理批量请求的方法,即将每个子请求的头部和正文分别放置在主请求的相应部分中,并使用类似于“--batch_xxxx”的边界。Google和Facebook的解决方案之间是否存在关键差异?此外,“将一个请求的响应用作另一个请求的输入”听起来非常有趣,您介意分享更多细节吗?或者应该在哪种场景下使用? - Yang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接