休息集合中的分页

我感兴趣的是将一个直接的 REST 接口公开给 JSON 文档集合(想想 CouchDB坚持下去)。我遇到的问题是,如果集合很大,如何处理集合根目录上的 GET操作。

作为一个例子,假设我正在公开 StackOverflow 的 Questions表,其中每一行都被公开为一个文档(并不一定有这样一个表,只是一个大量“文档”集合的具体例子)。收集将提供在 /db/questions与通常的 CRUD api GET /db/questions/XXXPUT /db/questions/XXXPOST /db/questions是在发挥。获取整个集合的标准方法是使用 GET /db/questions,但是如果将每一行作为 JSON 对象转储,那么您将获得相当大的下载量,并且服务器部分将进行大量工作。

当然,解决方案是分页。Dojo 已经在其 JsonRestStore中解决了这个问题,方法是通过一个聪明的兼容 RFC2616的扩展,即使用带有定制范围单元 itemsRange报头。结果是只返回请求范围的 206 Partial Content。与查询参数相比,这种方法的优势在于它将查询字符串留给... 查询(例如,GET /db/questions/?score>200或某些类似的查询,是的,这将被编码为 %3E)。

这种方法完全涵盖了我想要的行为。问题是,RFC 2616在206响应(重点是我的)中指明:

请求必须包含一个 Range 头字段(第14.35节) 指示所需的范围,并且可能包括一个 If-Range 头字段(第14.27节)使请求有条件。

这在标题的标准用法的上下文中是有意义的,但是这是一个问题,因为我希望206响应默认用于处理天真的客户机/随机浏览者。

我仔细研究了 RFC,希望找到一个解决方案,但是对我的解决方案不满意,我对 SO 对这个问题的看法很感兴趣。

我有一些想法:

  • 返回带有 Content-Range头的 200!我不认为这是错误的,但是我更希望有一个更明显的指示,即响应只是部分内容。
  • 返回 400 Range Required -对于所需的头没有特殊的400响应代码,因此必须使用默认错误并手动读取。这也使得通过 Web 浏览器(或者其他客户端,比如 Resty)进行探索变得更加困难。
  • 使用查询参数 -标准方法,但是我希望允许使用类似 Persever 的查询,这样就切入了查询名称空间。
  • 返回 206!我认为大多数客户不会抓狂,但是我不想违反 RFC 中的必须条款
  • 扩大规格!返回 266 Partial Content -行为与206完全相同,但是响应的请求必须不包含 Range头。我认为266已经足够高了,我不应该遇到碰撞问题,这对我来说是有意义的,但我不清楚这是否被认为是禁忌。

我认为这是一个相当普遍的问题,我希望看到这个问题以一种事实上的方式解决,这样我或其他人就不会有重造轮子。

当集合很大时,通过 HTTP 公开完整集合的最佳方式是什么?

38104 次浏览

我的直觉是,HTTP 范围扩展不是为您的用例设计的,因此您不应该尝试。部分响应意味着 206,只有在客户机请求时才必须发送 206

您可能需要考虑一种不同的方法,比如 Atom 中的一种使用方法(其中设计的表示可能是部分的,并且返回状态 200和可能的分页链接)。参见 RFC 4287RFC 5005

您可能会考虑使用类似于 Atom Feed Protocol 的模型,因为它有一个健全的 HTTP 集合模型以及如何操作它们(其中“疯狂”指的是 WebDAV)。

还有定义集合模型和 REST 操作的 原子发布协议,另外还可以使用 RFC 5005-馈送分页和归档在大型集合中进行分页。

从 Atom XML 切换到 JSON 内容应该不会影响这个想法。

编辑:

在进一步考虑之后,我倾向于认为 Range Header 不适合分页。其逻辑是,Range 标头用于服务器的响应,而不是应用程序。如果您提供了100MB 的结果,但是服务器(或客户机)一次只能处理1MB 的结果,那么,这就是 Range 头的用途。

我也认为资源的一个子集就是它自己的资源(类似于关系代数)因此它应该在 URL 中得到表示。

因此,基本上,我撤回了我原来的答案(下面)关于使用标题。


我认为你回答了你自己的问题,或多或少-返回200或206与内容范围和可选地使用一个查询参数。我将嗅探用户代理和内容类型,并根据它们检查查询参数。否则,请求范围标头。

实际上,你的目标是相互冲突的——让人们使用他们的浏览器进行浏览(这不容易允许自定义标题) ,或者强迫人们使用一个可以设置标题的特殊客户端(这不允许他们进行浏览)。

你可以根据请求为他们提供一个特殊的客户端——如果它看起来像一个普通的浏览器,发送一个小的 ajax 应用程序来渲染页面并设置必要的标题。

当然,关于 URL 是否应该包含此类事情所需的所有状态,也存在争议。使用标头指定范围可能会被某些人认为是“不平静的”。

顺便说一句,如果服务器能够响应一个“ Can-Specify: Header1,header2”头,并且 Web 浏览器将显示一个 UI,这样用户可以填充值(如果他们需要的话) ,那将是非常好的。

您可以检测 Range标头,如果存在则模拟 Dojo,如果不存在则模拟 Atom。在我看来,这样可以很好地划分用例。如果您正在响应来自应用程序的 REST 查询,那么您希望它使用 Range标头进行格式化。如果您正在响应一个随意的浏览器,那么如果您返回页面链接,它将让该工具提供一个简单的方法来探索集合。

如果有超过一页的回复,并且您不想同时提供整个集合,这是否意味着有多个选择?

在对 /db/questions的请求中,返回带有 Link标头的 300 Multiple Choices,这些标头指定如何访问每个页面以及带有 URL 列表的 JSON 对象或 HTML 页面。

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

对于每个页面的结果,都有一个 Link头(空字符串表示当前 URL,每个页面的 URL 是相同的,只是访问范围不同) ,关系定义为 根据即将到来的 Link规格定制一个。这种关系将解释您的定制 266,或您的违反 206。这些头是机器可读的版本,因为所有示例都需要一个理解的客户端。

(如果您坚持“范围”路线,我相信您自己的 2xx返回代码,正如您所描述的,将是最好的行为在这里。您应该为您的应用程序这样做,诸如“ HTTP 状态代码是可扩展的”而且你有很好的理由。)

300 Multiple Choices说你也应该为用户代理提供一个主体来选择。如果您的客户端理解,它应该使用 Link标头。如果是一个用户手动浏览,也许是一个 HTML 页面链接到一个特殊的“分页”根资源,可以处理呈现该特定网页的基础上的 URL?/humanpage/1/db/questions还是什么可怕的东西?


Richard Levasseur 博客上的评论让我想起了另外一个选项: Accept头文章(第14.1节)。回到 oEmbed 规范出来的时候,我想知道为什么没有完全使用 HTTP 来完成,并且写了一个使用它们的替代方案。

保留 300 Multiple ChoicesLink头文件和 HTML 页面作为初始的初始 HTTP GET,但不要使用范围,而是让新的分页关系定义 Accept头文件的使用。您随后的 HTTP 请求可能如下所示:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

Accept头允许您定义可接受的内容类型(您的 JSON 返回) ,以及该类型的可扩展参数(您的页码)。在我的 oEmbed 写作笔记(这里不能链接到它,我会在我的配置文件中列出它)的基础上,你可以非常明确地在这里提供一个 spec/relations 版本,以备将来需要重新定义 page参数的含义时使用。

我认为这里真正的问题是规范中没有告诉我们如何在面对413请求的实体太大时进行自动重定向。

最近我也在为同样的问题而挣扎,我在 RESTful Web 服务的书中寻找灵感。就个人而言,我不认为206是合适的,因为标题的要求。我的想法也引导我到300,但我认为这是更多的为不同的哑剧类型,所以我看了理查德森和露比必须在附录 B 的主题,第377页说。他们建议服务器只选择首选的表示形式并将其返回200,基本上忽略了应该是300的概念。

这也符合链接到我们从原子中获得的下一个资源的概念。我实现的解决方案是向我发送回来的 json 映射添加“ next”和“ before”键,然后就完成了。

后来我开始考虑,也许应该做的事情是发送一个307——临时重定向到一个类似于/db/questions/1,25的链接——将原始 URI 保留为规范资源名,但是它会让您找到一个适当命名的从属资源。这是我希望看到的413行为,但307似乎是一个很好的妥协。实际上还没有在代码中尝试过这种方法。更好的做法是将重定向重定向到包含最近提出的问题的实际 ID 的 URL。例如,如果每个问题都有一个整数 ID,系统中有100个问题,您想显示最近的10个问题,那么对/db/questions 的请求应该是307对/db/questions/100,91

这是一个非常好的问题,谢谢你的提问。你向我证实了我没有因为花了几天时间思考这个问题而发疯。

在我看来,实现这一点的最佳方法是将 range 包含为查询参数。例如,GET/db/questions/? date > mindate & date < maxdate。在没有查询参数的/db/questions/的 GET 上,返回带有 位置:/db/questions/? query-properties-to-review-the-default-page的303。然后提供一个不同的 URL,通过这个 URL,任何使用你的 API 的人都可以获得关于集合的统计数据(例如,如果他/她想要整个集合,那么使用什么查询参数) ;

我不太同意你们中的一些人。我已经为 REST 服务的这个特性工作了几个星期。我最后做的事很简单。我的解决方案只适用于 REST 人员所称的集合。

客户端必须包含一个“ Range”头,以指示他需要集合的哪一部分,否则,当被请求的集合太大而无法在一个往返过程中检索时,就可以处理413 REQUESTED ENTITY TOO LARGE 错误。

服务器发送一个206 PARTIAL CONTENT 响应,其中 CONTENT-Range 标头指定资源的哪一部分已经发送,ETag 标头标识集合的当前版本。我通常使用一个类似 Facebook 的 ETag { last _ Amendment _ time戳}-{ resource _ id } ,我认为集合的 ETag 是它包含的最新修改的资源的 ETag。

要请求一个集合的特定部分,客户端必须使用“ Range”头,并用从先前执行的请求中获得的集合的 ETag 填充“ If-Match”头,以获取同一集合的其他部分。因此,服务器可以在发送请求的部分之前验证集合是否已更改。如果存在更新的版本,则返回412 PRECONDITION FAILED 响应,以邀请客户端从头检索集合。这是必要的,因为这可能意味着在当前请求的部分之前或之后可能已经添加或删除了一些资源。

我使用 ETag/If-Match 和 Last-Amendment/If-UnAmendment-since 来优化缓存。浏览器和代理可能依赖其中一个或两个来实现缓存算法。

我认为一个 URL 应该是干净的,除非它包含一个搜索/过滤查询。仔细想想,搜索只不过是集合的部分视图。取代汽车/搜索? q = BMW 类型的 URL,我们应该看到更多的汽车? 制造商 = BMW。

您仍然可以使用 200响应代码返回 Accept-RangesContent-Ranges。这两个响应头为 推理提供了足够的信息,与 206响应代码显式提供的信息相同。

我将使用 Range进行分页,并让它简单地返回一个简单的 GET200

这感觉100% RESTful 还有并不使浏览更加困难。

编辑: 我写了一篇关于这个的博客文章: 《 http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html 》

随着 Rfc723x未注册的范围单位确实违反了规范中的明确建议的发布,考虑 Rfc7233(不赞成 rfc2616) :

新的射程单位应该在 IANA 注册”(连同对 HTTP 范围单元注册表的引用)。

范围标头的一个大问题是,许多公司代理会过滤掉它们。我建议使用查询参数。

虽然可以为此目的使用 Range 头,但我不认为这是目的所在。它似乎是为处理脆弱的连接和限制数据而设计的(这样,如果缺少某些东西或者大小过于庞大而无法处理,客户机就可以请求部分请求)。您正在将分页编入通信层中可能用于其他目的的内容。 处理分页的“正确”方法是使用返回的类型。您应该返回一个新类型,而不是返回疑问句对象。

如果问题是这样的:

< 问题 > < 问题指数 = 1 > < 问题指数 = 2 > ... 问题 > 代码 >

新的类型可能是这样的:

< 问题页 > < startIndex > 50 < 返回的数目 > 10 < 计数总数 > 1203 < 问题 > < 问题指数 = 50 > < 问题指数 = 51 > .. < 问题页 >

当然,您可以控制媒体类型,这样就可以使您的“页面”成为适合您需要的格式。如果 make 是通用的,则可以在客户机上使用单个解析器来处理所有类型的相同分页。我认为这更符合 HTTP 规范的精神,而不是为了其他东西而捏造 Range 参数。