RESTful API 和批量操作

RESTful API and bulk operations

我有一个在共享数据库上执行 CRUD 操作的中间层。当我将产品转换为 .NET Core 时,我想我也会考虑将 REST 用于 API,因为 CRUD 应该是它做得很好的地方。看起来 REST 是一个很好的单记录操作解决方案,但是当我想删除,比如说,1,000 条记录时会发生什么?

每个专业的 multi-user 应用程序都会有一些乐观并发检查的概念:你不能让一个用户在没有反馈的情况下抹杀另一个用户的工作。据我了解,REST 使用 HTTP ETag header 记录处理此问题。如果客户端发送的 ETag 与服务器的标签不匹配,那么您将发出 412 Precondition Failed。到目前为止,一切都很好。但是我要删除1000条记录怎么办呢? 1,000 次单独调用的 back-and-forth 时间相当可观,那么 REST 如何处理涉及 乐观并发 的批处理操作?

REST 的重点是资源和客户端与服务器的解耦,尽管它不是简单的 CRUD 架构或协议。虽然 CRUD 和 REST 看起来非常相似,都是通过 REST 原则管理资源 can often also have sideeffects。因此,将 REST 描述为简单的 CRUD 是过于简单化了。

关于 batch-processing REST 资源,底层协议(通常是 HTTP)确实定义了可以使用的功能。 HTTP 定义了几个可用于修改多个资源的操作。

POST 是协议的 all-purpose、swiss-army 刀,可用于按您的喜好从字面上管理资源。由于语义由开发人员定义,您可以使用它一次创建、更新或删除多个资源。

PUT 具有用请求的负载 body 替换在给定 URI 处可获得的资源状态的语义。如果您向“列表”资源发送 PUT 请求并且有效负载定义了条目列表,您也可以实现批处理操作。

The fundamental difference between the POST and PUT methods is highlighted by the different intent for the enclosed representation. The target resource in a POST request is intended to handle the enclosed representation according to the resource's own semantics, whereas the enclosed representation in a PUT request is defined as replacing the state of the target resource.

...

A PUT request applied to the target resource can have side effects on other resources. For example, an article might have a URI for identifying "the current version" (a resource) that is separate from the URIs identifying each particular version (different resources that at one point shared the same state as the current version resource). A successful PUT request on "the current version" URI might therefore create a new version resource in addition to changing the state of the target resource, and might also cause links to be added between the related resources. (Source)

PATCH (RFC 5789) 尚未包含在 HTTP 协议中,尽管有很多框架支持。它主要用于一次更改多个资源或对资源执行部分更新,如果更新的部分是某个其他资源的 sub-resource,PUT 也可以实现;在这种情况下,它会对外部资源产生部分更新的影响。

重要的是要知道 PATCH 请求包含服务器必须完成的将资源转换为预期状态的必要步骤。因此,客户端必须获取当前状态并预先计算转换所需的必要步骤。关于这个主题的一个非常有用的博客 post 是 Don't patch like an idiot. Here JSON Patch (RFC) 是一种基于 JSON 的媒体类型,它清楚地可视化了 PATCH 概念。必须完全应用补丁请求(补丁请求中定义的每个操作)或根本不应用。因此,它需要事务范围内的处理和回滚,以防任何操作失败。

ETagIfModifiedSince headers 等条件请求在 RFC 7232 中定义,仅当请求应用于资源的最新版本,因此与(分布式)数据库中的乐观锁定相关。

So far, so good. But what do I use when I want to delete 1,000 records?

这取决于您将使用的框架。如果支持PATCH我明确投PATCH。如果没有,使用 POST 可能比 PUT 更安全,因为 PUT 具有非常严格的语义,因为语义已由您明确定义。在 batch-delete 的情况下,PUT 也可以通过将 collection 资源设为空 body 来使用,其结果是删除 [=58= 中的任何项目] 并因此清除整个 collection。如果某些项目应保留在 collection 中,那么 PATCHPOST 可能更易于使用。

如果我没理解错的话,您希望对每条记录单独进行乐观并发。也就是说,只有当记录的状态符合客户的期望时,才会删除每条记录。 (如果你只想断言整个集合的状态,那么 If-Match 和 412 就足够了。)

Roman Vottner 的回答很好地解释了所涉及的 HTTP 方法,但我会尝试填写一些细节。

买者自负

当我们谈论“REST 将如何处理”这个或那个时,您明白从技术上讲,您可以使用 HTTP 作为任何适合您的方式进行任何操作的传输。

因此,当您询问 REST 时,我假设您对 uniform interface 感兴趣 — 一种理论上可用于各种客户端和服务器的方法。

但那里的关键词是“理论上”。例如,一旦您定义了自己的媒体类型(您自己的 JSON 结构),很多统一性就会付之东流,因为客户端无论如何都必须根据您的特定 API 进行编码,在哪一点你可以让它跳过你想要的任何箍。

但是,如果您仍然对尽可能多地恢复一致性感兴趣,请继续阅读。

全有或全无

如果你想要一个全有或全无的操作,如果任何一个先决条件失败,它就会完全失败,那么,正如 Roman 所建议的,你可以使用 PATCH with the JSON Patch 格式。为此,您需要将您的集合概念表示为单个 JSON 对象,补丁将应用于该对象。

例如,假设您有 /my/collection/1/my/collection/4 等资源。您可以将 /my/collection/ 表示为:

{
    "resources": {
        "1": {
            "href": "1",
            "etag": "\"BRkDVtYw\"",
            "name": "Foo Bar",
            "price": 1234.5,
            ...
        },
        "4": {
            "href": "4",
            "etag": "\"RCi8knuN\"",
            "name": "Baz Qux",
            "price": 2345.6,
            ...
        },
        ...
    }
}

这里,"1""4"是相对于/my/collection/的URL。您可以改用特定于域的 ID,但适当的 REST 会根据不透明的 URLs.

进行操作

标准不要求您在 GET /my/collection/ 上实际提供此表示,但如果您支持此类请求,则应使用该表示。无论如何,您可以对这个结构应用以下 JSON 补丁:

PATCH /my/collection/ HTTP/1.1
Content-Type: application/json-patch+json

[
    {"op": "test", "path": "/resources/1/etag", "value": "\"BRkDVtYw\""},
    {"op": "remove", "path": "/resources/1"},
    {"op": "test", "path": "/resources/4/etag", "value": "\"RCi8knuN\""},
    {"op": "remove", "path": "/resources/4"},
    ...
]

这里,path不是URL路径,而是JSON pointer进入上述表示。

如果所有补丁操作都成功,那么您将返回一个成功的状态代码,例如 204 (No Content) or 200 (OK)

如果任何 ETag test 操作失败,在这种情况下您将响应 409 (Conflict). You should not respond with 412 (Precondition Failed),因为请求本身没有先决条件(如 If-Match)。

如果出现任何其他问题,您可以使用其他适当的状态代码进行响应:请参阅 RFC 5789 § 2.2 and RFC 7231 § 6.6

混合结果

如果您不想要“全有或全无”语义,那么我不知道有任何标准化解决方案。正如 Roman 所指出的,在这种情况下您不能使用 PATCH 方法,但您可以将 POST 与自定义媒体类型 (RFC 6838 § 3.4) 一起使用。它可能看起来像这样:

POST /my/collection/ HTTP/1.1
Content-Type: application/x.my-patch+json
Accept: application/x.my-patch-results+json

{
    "delete": [
        {"href": "1", "if-match": "\"BRkDVtYw\""},
        {"href": "4", "if-match": "\"RCi8knuN\""},
        ...
    ]
}

您可以使用 200(确定)响应此类请求,而不管任何单个删除是否成功。另一种选择是 207 (Multi-Status), but I don’t see any benefits to it in this case, and it’s not widely used outside of WebDAV, so Postel’s law 建议不要去那里。

HTTP/1.1 200 OK
Content-Type: application/x.my-patch-results+json

{
    "delete": [
        {"href": "1", "success": true},
        {"href": "4", "success": false, "error": {...}},
        ...
    ]
}

当然,如果补丁一开始就无效,您应该酌情回复 415 (Unsupported Media Type) or 422 (Unprocessable Entity)

另一个角度

The back-and-forth time for 1,000 individual calls is considerable

它在 HTTP/1.1。但是,如果您可以使用 HTTP/2——它对并发请求有更好的支持,并且每个请求的网络开销要小得多——那么 1000 个单独的请求可能对你来说就足够了。