RESTful API 和批量操作



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

每个专业的多用户应用程序都会有一些乐观并发检查的概念:你不能让一个用户在没有反馈的情况下清除另一个用户的工作。 据我了解,REST 使用 HTTP ETag 标头记录处理此问题。 如果客户端发送的 ETag 与服务器的标记不匹配,则发出412 前提条件失败。 目前为止,一切都好。 但是,当我想删除 1,000 条记录时,我该使用什么? 1,000 次单独调用的来回时间相当长,那么 REST 如何处理涉及乐观并发的批处理操作呢?

REST的重点是资源和客户端与服务器的分离,尽管它不是一个简单的CRUD架构或协议。虽然 CRUD 和 REST 看起来非常相似,但通过 REST 原则管理资源通常也会产生副作用。因此,将 REST 描述为简单的 CRUD 事物是一种过于简化。

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

POST是该协议的通用瑞士军刀,可用于根据自己的喜好管理资源。由于语义是由开发人员定义的,因此您可以使用它一次创建、更新或删除多个资源。

PUT具有将给定 URI 处可获取的资源的状态替换为请求的有效负载正文的语义。如果将PUT请求发送到"列表"资源,并且有效负载定义了条目列表,则也可以实现批处理操作。

POST 和 PUT 方法之间的根本区别是 由所包含表示的不同意图突出显示。 POST 请求中的目标资源旨在处理 根据资源自身语义的封闭表示, 而 PUT 请求中的封闭表示定义为 替换目标资源的状态。

应用于目标资源的 PUT 请求可能会对 其他资源。 例如,一篇文章可能具有 标识独立于 标识每个特定版本的 URI(同一点与当前版本共享相同状态的不同资源 资源)。 对"当前版本"URI 的成功 PUT 请求 因此,除了更改之外,还可能创建一个新版本资源 目标资源的状态,并且还可能导致链接 在相关资源之间添加。 (来源)

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

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

ETagIfModifiedSince标头等条件请求在 RFC 7232 中定义,仅当请求应用于最新版本的资源时,才能在 HTTP 请求中使用条件请求来执行修改,因此与(分布式)数据库中的乐观锁定相关。

目前为止,一切都好。但是,当我想删除 1,000 条记录时,我该使用什么?

这取决于您将使用的框架。如果它支持PATCH我显然投票支持PATCH.如果没有,您可能比PUT更安全地使用POST因为PUT具有非常严格的语义,因为语义是由您明确定义的。在批量删除的情况下,还可以通过将集合资源定位为空主体来使用PUT,其结果是删除集合中的任何项,从而清除整个集合。但是,如果某些项目应保留在集合中,则PATCHPOST可能更易于使用。

如果我理解正确,您希望对每条记录单独进行乐观并发。也就是说,仅当每条记录的状态与客户端的期望匹配时,才会删除该记录。(如果只想断言整个集合的状态,则If-Match和 412 就足够了。

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

Caveat emptor

当我们谈论"REST 如何处理"这个或那个时,您就会明白,从技术上讲,您可以使用 HTTP 作为任何适合您的操作的传输。

因此,当您询问REST时,我假设您对统一接口感兴趣 - 理论上可以由一系列不同的客户端和服务器使用的方法。

但那里的关键词是"理论上"。例如,一旦你定义了自己的媒体类型(你自己的JSON结构),很多一致性就会付诸东流,因为无论如何都必须针对你的特定API对客户端进行编码,此时你可以要求它跳过你想要的任何环节。

但是,如果您仍然有兴趣尽可能多地挽救均匀性,请继续阅读。

有或全无

如果你想要一个全有或全无的操作,如果任何一个单独的前提条件失败,它就会完全失败,那么,正如 Roman 所建议的那样,你可以将 PATCH 与 JSON 补丁格式一起使用。为此,您需要将集合的概念表示为要应用修补程序的单个 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 会根据不透明的 URL 进行操作。

标准并不要求您在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 指针。

如果所有修补程序操作都成功,则使用成功的状态代码(如 204(无内容)或 200(正常))进行响应。

如果任何 ETagtest操作失败,则使用 409(冲突)进行响应。在这种情况下,您不应该使用 412(前提条件失败)进行响应,因为请求本身没有前提条件(如If-Match)。

如果出现任何其他错误,您可以使用其他适当的状态代码进行响应:请参阅 RFC 5789 § 2.2 和 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(多状态),但在这种情况下我看不到它的任何好处,而且它在 WebDAV 之外没有广泛使用,所以 Postel 定律建议不要去那里。

HTTP/1.1 200 OK
Content-Type: application/x.my-patch-results+json
{
"delete": [
{"href": "1", "success": true},
{"href": "4", "success": false, "error": {...}},
...
]
}

当然,如果修补程序一开始就无效,则应根据需要使用 415(不支持的媒体类型)或 422(无法处理的实体)进行响应。

另一个角度

1,000 个单独呼叫的来回时间相当长

它采用 HTTP/1.1 格式。但是,如果你可以使用HTTP/2 - 它对并发请求有更好的支持,以及每个请求的网络开销更小 - 那么1000个单独的请求可能对你来说很好。

最新更新