在 REST Web 服务中处理批处理操作的模式?

对于 REST 风格 Web 服务中的资源批处理操作,存在哪些经过验证的设计模式?

我试图在理想和现实之间找到一个平衡点在表现和稳定性方面。我们现在已经有了一个 API,其中所有操作要么从列表资源(即: GET/user)检索,要么在单个实例(PUT/user/1、 DELETE/user/22等)上检索。

在某些情况下,您希望更新整个对象集的单个字段。来回发送每个对象的整个表示形式来更新一个字段似乎非常浪费。

在 RPC 样式的 API 中,您可以有一个方法:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc.

这里的 REST 等价于什么?或者偶尔妥协一下也没关系。添加一些真正提高性能的特定操作是否会破坏设计,等等?现在所有情况下的客户端都是 Web 浏览器(客户端的 javascript 应用程序)。

91522 次浏览

一点也不——我认为 REST 等价物(或者至少有一种解决方案)几乎完全相同——一个专门设计的接口可以满足客户机所需的操作。

我想起了 Crane 和 Pascarello 的书《 Ajax 在行动》(顺便说一下,这是一本非常好的书——强烈推荐)中提到的一种模式,在这本书中,他们阐述了如何实现一种 指挥队列类型的对象,这种对象的工作就是将请求排队成批,然后定期将它们发送到服务器。

如果我没记错的话,这个对象实际上只是保存了一个“命令”数组——例如,为了扩展你的示例,每个对象都包含一个“ markAsRead”命令、一个“ messageId”记录,可能还有一个对回调/处理函数的引用——然后根据某个时间表,或者根据某个用户操作,命令对象将被序列化并发布到服务器,客户端将处理后续处理。

我手边没有这些细节,但是这听起来像是这种类型的命令队列是处理你的问题的一种方法; 它将大大减少整体的聊天,并且它将抽象服务器端的接口,在某种程度上你可能会发现更灵活的方式。


更新 : 啊哈!我在网上找到了这本书的一个片段,包括代码示例(尽管我仍然建议选择真正的书!).看看这里,从5.5.3节开始:

这很容易编码,但可能导致 很多非常小的流量 服务器,这是低效率和 可能会让人困惑,如果我们想的话 控制我们的交通,我们可以捕获 这些更新和 在本地排队 然后发送到服务器 在我们有空的时候批量生产。一个简单的 用 JavaScript 实现的更新队列 如清单5.13所示。[ ... ]

队列维护两个数组 是一个数字索引数组,可以 附加了哪些新的更新 是一个关联数组,包括 那些已经发送到 服务器,但它正在等待一个 回答。

下面是两个相关的函数——一个负责向队列添加命令(addCommand) ,另一个负责序列化命令并将其发送到服务器(fireRequest) :

CommandQueue.prototype.addCommand = function(command)
{
if (this.isCommand(command))
{
this.queue.append(command,true);
}
}


CommandQueue.prototype.fireRequest = function()
{
if (this.queued.length == 0)
{
return;
}


var data="data=";


for (var i = 0; i < this.queued.length; i++)
{
var cmd = this.queued[i];
if (this.isCommand(cmd))
{
data += cmd.toRequestString();
this.sent[cmd.id] = cmd;


// ... and then send the contents of data in a POST request
}
}
}

这应该能让你振作起来,祝你好运!

在类似您的示例中的操作中,我很想编写一个范围解析器。

创建一个能够读取“ messageIds = 1-3,7-9,11,12-15”的解析器并不是很麻烦。它肯定会提高覆盖所有消息的一揽子操作的效率,并且更具可伸缩性。

一个简单的批处理 RESTful 模式是利用集合资源。例如,一次删除多条消息。

DELETE /mail?&id=0&id=1&id=2

批量更新部分资源或资源属性要复杂一些。也就是说,更新每个 markedAsRead 属性。基本上,不是将属性视为每个资源的一部分,而是将其视为一个存放资源的桶。其中一个例子已经发布了。我稍微调整了一下。

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

基本上,您是在更新标记为已读的邮件列表。

您还可以使用它将多个项目分配给同一类别。

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

显然,做 iTunes 风格的批量部分更新要复杂得多(例如,艺术家 + 专辑标题,但不是跟踪标题)。桶的类比开始失效了。

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

从长远来看,更新单个部分资源或资源属性要容易得多。只要利用一个子资源。

POST /mail/0/markAsRead
POSTDATA: true

或者,您可以使用参数化资源。这在 REST 模式中不太常见,但在 URI 和 HTTP 规范中是允许的。分号将资源中水平相关的参数分隔开。

更新几个属性,几个资源:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

更新多个资源,只有一个属性:

POST /mail/0;1;2/markAsRead
POSTDATA: true

更新多个属性,只更新一个资源:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

REST 式的创造力无处不在。

虽然我认为“ Alex 正在走正确的道路,但从概念上来说,我认为这应该是建议的反面。”。

网址实际上就是“我们的目标资源”,因此:

    [GET] mail/1

意味着从 id 为1的邮件中获取记录

    [PATCH] mail/1 data: mail[markAsRead]=true

查询字符串是一个“过滤器”,过滤从 URL 返回的数据。

    [GET] mail?markAsRead=true

所以我们请求所有已经标记为已读的邮件。因此,[ PATCH ]到这个路径将说“补丁记录 已经标记为真”... 这不是我们正在努力实现的。

因此,遵循这种思路的批处理方法应该是:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

当然,我并不是说这就是真正的 REST (它不允许批处理记录操作) ,而是遵循 REST 已经存在并正在使用的逻辑。

好帖子。这几天我一直在寻找解决办法。我想出了一个解决方案,使用一个查询字符串传递一堆 ID,这些 ID 用逗号分隔,比如:

DELETE /my/uri/to/delete?id=1,2,3,4,5

然后将其传递给 SQL 中的 WHERE IN子句。它工作得很好,但不知道其他人对这种方法有什么看法。

你的语言,“它 看起来非常浪费...”,对我来说,表明一个过早优化的尝试。除非能够证明发送对象的整个表示形式对性能有很大影响(我们说的是用户不能接受的大于150ms) ,否则尝试创建一个新的非标准 API 行为是没有意义的。记住,API 越简单,就越容易使用。

对于删除,请发送以下内容,因为服务器在执行删除之前不需要了解对象的任何状态。

DELETE /emails
POSTDATA: [{id:1},{id:2}]

下一个想法是,如果应用程序遇到了与对象大容量更新有关的性能问题,那么应该考虑将每个对象分解为多个对象。这样一来,JSON 的有效负载只是其大小的一小部分。

举例来说,当你发送回应以更新两个独立电子邮件的“已读”和“存档”状态时,你必须发送以下信息:

PUT /emails
POSTDATA: [
{
id:1,
to:"someone@bratwurst.com",
from:"someguy@frommyville.com",
subject:"Try this recipe!",
text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
read:true,
archived:true,
importance:2,
labels:["Someone","Mustard"]
},
{
id:2,
to:"someone@bratwurst.com",
from:"someguy@frommyville.com",
subject:"Try this recipe (With Fix)",
text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
read:true,
archived:false,
importance:1,
labels:["Someone","Mustard"]
}
]

我会把电子邮件中可变的组件(阅读、存档、重要性、标签)拆分成一个单独的对象,因为其他组件(到、从、主题、文本)永远不会被更新。

PUT /email-statuses
POSTDATA: [
{id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
{id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
]

另一种方法是利用 PATCH 的使用。显式指示打算更新哪些属性,以及应忽略所有其他属性。

PATCH /emails
POSTDATA: [
{
id:1,
read:true,
archived:true
},
{
id:2,
read:true,
archived:false
}
]

人们声明 PATCH 应该通过提供包含: action (CRUD)、 path (URL)和 value change 的更改数组来实现。这可能被认为是一个标准的实现,但是如果您查看整个 REST API,就会发现它是非直观的一次性实现。此外,上面的实现是如何 GitHub 已经实现了 PATCH

总而言之,通过批处理操作坚持 RESTful 原则并保持可接受的性能是可能的。

Google drive API 有一个非常有趣的系统来解决这个问题(看这里)。

它们所做的基本上是将不同的请求分组到一个 Content-Type: multipart/mixed请求中,每个完整的请求由一些定义的分隔符分隔。批处理请求的头和查询参数被继承到各个请求(即 Authorization: Bearer some_token) ,除非它们在各个请求中被重写。


示例 : (取自其 医生)

要求:

POST https://www.googleapis.com/batch


Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963


--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary




POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8




{
"emailAddress":"example@appsrocks.com",
"role":"writer",
"type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary




POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8




{
"domain":"appsrocks.com",
"role":"reader",
"type":"domain"
}
--END_OF_PART--

回应:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1




HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35




{
"id": "12218244892818058021i"
}




--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2




HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35




{
"id": "04109509152946699072k"
}




--batch_6VIxXCQbJoQ_AATxy_GgFUk--

从我的角度来看,我认为 Facebook 有最好的实现。

使用批处理参数和令牌参数发出单个 HTTP 请求。

在批处理中发送一个 json,它包含一组“请求”。 每个请求都有一个方法属性(get/post/put/delete/etc...)和一个 relant _ url 属性(端点的 uri) ,另外 post 和 put 方法允许一个“ body”属性,用于发送要更新的字段。

更多信息请访问: Facebook 批处理 API