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

在REST风格的Web服务中对资源进行批量操作的devise模式是什么?

我试图在性能和稳定性方面在理想与现实之间取得平衡。 我们现在有一个API,其中所有的操作都可以从列表资源(即GET / user)或单个实例(PUT / user / 1,DELETE / user / 22等)中检索。

有些情况下,你想更新一整套对象的单个字段。 来回发送每个对象的整个表示来更新一个字段是非常浪费的。

在RPC风格的API中,可以有一个方法:

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

这里的REST是什么? 或者现在和之后妥协是可以的。 它是否破坏devise,添加一些特定的操作,真正提高性能等? 所有情况下,客户端现在是一个Web浏览器(客户端的JavaScript应用程序)。

一个简单的批处理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风格批量部分更新显然要复杂得多(例如,artist + albumTitle,而不是trackTitle)。 桶比喻开始分解。

 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 

RESTful创造力丰富。

根本不是 – 我认为REST等价物(或者至less有一个解决scheme)几乎就是这样 – 一个专门devise的界面适合客户所需的操作。

我想起了Crane和Pascarello所着的“ Ajax in Action”一书中提到的一种模式(顺便提一下,这是一本非常棒的书),在这本书中,他们演示了如何实现一个CommandQueue类的对象,然后定期将其发布到服务器。

对象,如果我没有记错的话,本质上只是举行一个“命令”的数组 – 例如,扩展你的例子,每一个logging包含一个“markAsRead”命令,一个“messageId”,也许一个callback/处理程序函数 – 然后根据某个时间表或某些用户操作,命令对象将被序列化并发布到服务器,客户端将处理随后的后处理。

我并没有碰巧得到细节,但是这听起来像是一个这样的命令队列将是处理你的问题的一种方法。 它会大大降低整体的烦琐程度,并且会以一种更为灵活的方式抽象出服务器端的接口。


更新 :啊! 我从网上find了一本关于代码示例的书(尽pipe我仍然build议你拿起实际的书)。 看看这里 ,从第5.5.3节开始:

这很容易编码,但可能导致很多非常小的stream量到服务器,这是低效率的,可能会令人困惑。 如果我们想要控制stream量,我们可以捕获这些更新,并将它们排队在本地 ,然后在休闲时分批发送给服务器。 列表5.13显示了一个用JavaScript实现的简单更新队列。 […]

队列维护两个数组。 queued是一个数字索引数组,附加了新的更新。 sent是一个关联数组,包含已发送到服务器但等待回复的更新。

这里有两个相关的函数 – 一个负责将命令添加到队列( 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 } } } 

这应该让你走。 祝你好运!

虽然我认为@Alex是一条正确的道路,但从概念上讲,我认为它应该与所build议的相反。

url实际上是“我们所针对的资源”,因此:

  [GET] mail/1 

意味着从ID为1的邮件中获取logging

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

意味着用ID 1修补邮件logging。查询string是一个“filter”,过滤从URL返回的数据。

  [GET] mail?markAsRead=true 

所以在这里我们要求所有已经标记为已读的邮件。 所以对[PATCH]这条道路来说就是“修补已经被标记为真的logging”……这不是我们想要实现的。

所以一个批处理方法,应该是这样的:

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

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

你的语言,“这似乎是非常浪费……”,对我来说,表示尝试过早优化。 除非可以发现,发送对象的整个表示forms是一个主要的性能问题(我们认为用户无法接受> 150ms),那么尝试创build新的非标准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应该通过提供一系列更改来实现,包括:动作(CRUD),path(URL)和值更改。 这可能被认为是一个标准的实现,但如果你看一下REST API的全部内容,这是一个非直观的一次性的。 另外,上面的实现是GitHub已经实现了PATCH 。

总而言之,可以通过批量操作来遵循RESTful原则,并且仍然具有可接受的性能。

谷歌驱动器API有一个非常有趣的系统来解决这个问题( 见这里 )。

他们所做的基本上是将不同的请求分组在一个Content-Type: multipart/mixed请求中,每个完整的请求由一些定义的分隔符分隔。 批量请求的标题和查询参数被inheritance到各个请求(即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-- 

我会试图像你的例子中的操作来编写一个范围parsing器。

编写一个可以读取“messageIds = 1-3,7-9,11,12-15”的parsing器并不麻烦。 这肯定会提高涵盖所有消息的全面操作的效率,并且更具可扩展性。

伟大的职位。 我一直在寻找一个解决scheme几天。 我想出了一个解决scheme,使用传递查询string与逗号分隔的一堆ID,如:

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

…然后将其传递给我的SQL中的WHERE IN子句。 它工作的很好,但不知道别人怎么看待这种方法。