Rest集合中的分页问题

137

我有兴趣公开将多个JSON文档集合暴露为直接的REST接口(类似于CouchDBPersevere)。我遇到的问题是如何处理集合根节点的GET操作,如果集合很大会怎么样。

举例来说,假设我正在公开StackOverflow的Questions表格,其中每一行都作为一个文档公开(并不一定有这样的表格,只是一个具体的“文档”数量可观的集合的示例)。该集合将在/db/questions下提供,并使用通常的CRUD api,例如GET /db/questions/XXXPUT /db/questions/XXX以及POST /db/questions。获取整个集合的标准方法是GET /db/questions,但是如果轻率地将每一行作为JSON对象转储,则会得到相当大的下载量并且服务器也需要进行大量工作。

当然,解决方案是分页。Dojo通过使用自定义范围单元“items”的Range头的巧妙RFC2616兼容扩展,在其JsonRestStore中解决了此问题。结果是一个返回仅请求的范围的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这样的其他客户端)进行探索更加困难。
  • 使用查询参数 - 标准方法,但我希望允许类似于Persevere的查询,这会占用查询命名空间。
  • 只需返回206 - 我认为大多数客户端不会感到恐慌,但我不想违反RFC中的MUST规定
  • 扩展规范!返回266 Partial Content - 与206完全相同,但是响应请求不能包含Range头。我认为266足够高,不会遇到冲突问题,并且对我来说很有意义,但我不清楚这是否被认为是禁忌。

我认为这是一个相当普遍的问题,我希望以某种事实上的方式解决这个问题,这样我或其他人就不必重新发明轮子。

在集合很大的情况下,公开完整集合的最佳方法是什么?


22
哇,这是一个很好的例子,说明在提出这个问题之前已经进行了一些认真的思考。 - Heiko Rupp
可能是REST Web应用中的分页的重复问题。 - rds
2
就Dojo在使用范围头的方法而言,尽管Accept-Ranges允许扩展,但从我所知道的所有情况来看,Range的EBNF并不允许:http://tools.ietf.org/html/rfc2616#section-14.35.2 。规范指出Range = "Range" ":" ranges-specifier,其中http://tools.ietf.org/html/rfc2616#section-14.35.1中的后者仅被描述为“byte-ranges-specifier”,它必须以“bytes-unit”开头,该字符串被定义为“bytes”。 - Brett Zamir
2
Content-Range 头部适用于请求体(可用于上传大文件等请求时,或下载响应时)。Range 头部用于请求特定范围。当请求中包含 Range 头部时,应该使用 206 响应。如果没有,则响应仍可以包括 Content-Range 头部,但响应代码应为 200。这个头部实际上似乎非常适合分页。 - Stijn de Witt
1
但是RFC 2616本身指出,“HTTP/1.1实现可以忽略使用其他单位指定的范围。”因此,使用范围标头进行分页是否是一个好的实践?因为这可能会影响互操作性。 - chetan choulwar
12个回答

33

我不完全同意你们中的一些人的看法。我已经为我的REST服务的一些功能工作了几周。我最终采取的解决方案非常简单。我的解决方案只对REST人员称为集合的内容有意义。

客户端必须包含一个“范围”头,以指示他需要集合的哪个部分,否则就要准备处理413请求过大错误,当所请求的集合太大无法在一次往返中检索时。

服务器会发送一个206部分内容响应,其中Content-Range头指定已发送的资源的哪个部分,并使用ETag头标识集合的当前版本。我通常使用类似Facebook的ETag {last_modification_timestamp}-{resource_id},并认为集合的ETag是其包含的最近修改资源的ETag。

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

我将ETag/If-Match与Last-Modified/If-Unmodified-Since配对以优化缓存。浏览器和代理服务器可能会依赖其中一个或两个算法进行缓存。

我认为除了包含搜索/筛选查询之外,URL应该保持简洁。如果你仔细想想,搜索只是集合的部分视图。我们应该看到更多类似于cars?manufacturer=BMW这样的URL,而不是cars/search?q=BMW。


1
@Mohamed 我想你指的是 If-Unmodified-Since,它对应于 E-Tag 变体的 If-Match,而不是 If-Modified-Since。话虽如此,根据您的用例,您也可以考虑删除此约束。比如说,如果您有一个只从顶部增长的集合(类似于某些“最新的第一”风格的集合),如果该集合在请求之间发生更改,最坏的情况就是翻页查看集合的用户会看到重复的条目。(这本身也是有用的信息:它告诉用户集合已经发生了变化) - Evgeniy Berezovsky
21
413状态码是“请求实体过大”,而不是“被请求的实体过大”。这意味着您的请求大小(例如上传文件)超过了服务器愿意处理的大小限制。因此,在此情况下使用413状态码似乎并不完全合适。 - user247702
@MickaelMarrache 响应中每个集合中包含的资源都会使用自己的 ETag 进行投影。 - Localist
你说“搜索只是集合的部分视图”。对于页面也是一样的。获取页面也可以称为“过滤”。只要页面资源与集合资源之间的关系清晰明确,URL中的页面参数就没有问题。例如,请参阅https://www.w3.org/community/hydra/wiki/Collection_Design#Pagination - letmaik
9
我强烈反对使用413。这是一个错误代码,意味着客户端由于大小问题而被服务器拒绝接受发送的某些东西。而不是相反!请参见 https://tools.ietf.org/html/rfc7231#section-6.5.11(注意它说的是**请求**有效载荷,而不是**响应**有效载荷)! - exhuma
显示剩余5条评论

24

我个人认为HTTP范围扩展并不适合你的用例,因此你不应该尝试使用。部分响应意味着206,而206只有在客户端请求时才能发送。

你可以考虑另一种方法,例如在Atom中使用的方法(其中表示设计上可能是部分的,并以状态200返回,以及可能的分页链接)。请参见RFC 4287RFC 5005


15
道场的使用完全符合规范。如果服务器无法理解“items”范围单位,则返回完整响应。我熟悉Atom,但这不是Rest分页的通用解决方案。这不是单个案例的解决方案,而更多地是通用解决方案的建议。并非所有文档/集合都适用于Atom模型,除非必须,否则没有强制使用的理由。 - Karl Guertin
1
@KarlGuertin 同意。可惜这被认为是被接受的答案,因为似乎社区中有很多人正在支持使用 RangeContent-Range 进行分页。 - Stijn de Witt

8
您仍然可以在200响应代码中返回Accept-RangesContent-Ranges。这两个响应头提供了足够的信息,使您能够推断出与206响应代码明确提供的相同信息。
我会使用Range进行分页,并让它简单地返回一个普通的GET200
这感觉完全符合RESTful,并且不会使浏览变得更加困难。
编辑: 我写了一篇关于这个的博客文章:http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

你如何使用这种方法与下一个部分进行通信?如果分页参数是查询的一部分,那么很容易向消费者提供元信息,例如下一页和最后一页。就像在这里看到的一样:https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api#pagination - The Fool

5
如果有多页响应,且您不想一次返回整个集合,那是否意味着有多个选择?
在请求 /db/questions 时,返回状态码为 300 Multiple Choices 的响应,并使用 Link 头指定如何获取每个页面以及包含 URL 列表的 JSON 对象或 HTML 页面。
Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

你需要为每个结果页面添加一个Link头部(空字符串表示当前URL,每个页面的URL相同,只是范围不同),关系定义为即将推出的自定义规范。这个关系可以解释你的自定义266或违反206。由于所有示例都需要理解客户端,因此这些头部是机器可读的版本。
(如果您坚持使用“范围”路线,我认为您自己描述的2xx返回代码将是最佳行为。您需要为您的应用程序等执行此操作["HTTP状态码是可扩展的。"],而且您有充分的理由。)

300多种选择表示您应该还提供一种方法,让用户代理选择主体。如果您的客户端理解,它应该使用链接标头。如果是手动浏览的用户,也许可以提供一个HTML页面,其中包含指向特殊的“分页”根资源的链接,该资源可以根据URL处理呈现特定页面?/humanpage/1/db/questions或类似丑陋的东西?


Richard Levasseur的帖子上的评论提醒我另一个选择:Accept头部(第14.1节)。当oEmbed规范出来时,我想知道为什么它没有完全使用HTTP,并撰写了一篇使用它们的替代方案。
保留“300多项选择”、“Link”标题和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撰写中的笔记(无法在此处链接,我将在我的个人资料中列出),您可以非常明确地提供一个规范/关系版本,在将来需要重新定义“page”参数的情况下使用。

1
+1 链接头,但我还建议使用常见的 first、prev、next、last rels,以及 RFC5005 的 prev-archive、next-archive 和 current。 - Joseph Holsten
在请求/db/questions时,返回300个多项选择和链接头,指定如何到达每个页面[...]。这种方法的问题(以及大多数纯REST设计的问题)是它会增加延迟。目标是尽量减少网络请求。第一个请求应该产生结果,而不是更多请求的链接,最终才能提供我们需要的数据。 - Stijn de Witt
请查看 ApiSuccessResponse 类。同时,您可以参考 Google 为 Android 创建的示例:https://github.com/android/architecture-components-samples/blob/main/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.kt - Rahul

4

编辑:

经过再次思考,我倾向于认为范围标头不适用于分页。这是因为范围标头是针对服务器响应而不是应用程序的。如果您提供了100兆字节的结果,但服务器(或客户端)一次只能处理1兆字节,那么这就是范围标头的作用。

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

所以基本上,我撤回了我的原始答案(如下)关于使用标头的问题。


我认为你或多或少地回答了自己的问题——返回200或206,带有内容范围,并可选择使用查询参数。我会嗅探用户代理和内容类型,并根据这些检查查询参数。否则,请要求范围标头。

你基本上有冲突的目标——让人们使用他们的浏览器进行探索(这不容易允许自定义标头),或者强制人们使用可以设置标头的特殊客户端(这不让他们进行探索)。

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

当然,还有关于URL是否应包含此类所有必要状态的争论。通过使用标头指定范围可能被某些人认为是“不具有RESTful性”。

顺便说一句,如果服务器能够响应“可以指定:Header1、header2”标头,那么Web浏览器将提供UI,以便用户填写值(如果他们需要)。


谢谢您的回复。我已经考虑过这个话题,但是希望能得到第二个意见。您有关于头文件参数的指针吗? - Karl Guertin
这是我收藏的唯一一个(请参见评论中的讨论):http://barelyenough.org/blog/2008/05/versioning-rest-web-services/另一个网站围绕 Ruby 对 .json、.xml、.whatever 的使用来确定请求的内容类型。其中一些示例:
  • 语言 - 将其放在 URL 中意味着将链接发送到其他国家会以错误的语言呈现它。
  • 分页 - 将其放在标头中意味着您无法将人们链接到您所看到的内容。
- Richard Levasseur
内容类型:语言和分页问题的组合 - 如果它在URL中,如果客户端不支持该内容类型(例如,.ajax和.html扩展名),怎么办?相反,如果URL中没有该内容类型,则无法确保给出相同的表示。 "新的ajax网站!example.com/cool.ajax"与"这里是很酷的文章:example.com/article.ajax#id=123"。 - Richard Levasseur
2
在我看来,无论它是什么,是否放在URL中取决于它的性质。我的一般规则是,如果它会识别一个具体的资源(无论是特定状态的资源、资源的选择还是离散结果),它就应该出现在URL中。搜索查询、分页和RESTful事务都是很好的例子。如果它是需要将抽象表示转换为具体表示所需的内容,则应将其放在头文件中。认证信息和内容类型是很好的例子。 - Richard Levasseur
我认为URL中的查询字符串是指定资源的查询选项。 - wprl

3
您可以考虑使用类似Atom Feed协议的模型,因为它具有对集合及其操作的合理HTTP模型(其中“不合理”意味着WebDAV)。
Atom发布协议定义了集合模型和REST操作,此外您还可以使用RFC 5005-Feed分页和存档来浏览大规模的集合。
从Atom XML切换到JSON内容不应影响这个想法。

3
我认为真正的问题在于规范中没有告诉我们如何在面对413 - 请求实体过大时进行自动重定向。我最近也遇到了同样的问题,我在《RESTful Web Services》一书中寻找灵感。个人认为206由于头部要求不适合使用。我的想法也引导我去看300,但我认为那更多是针对不同的mime类型,所以我查阅了Richardson和Ruby在附录B第377页上对此问题的建议。他们建议服务器只需选择首选表示并将其与200一起发送回来,基本上忽略了它应该是300的概念。这也符合我们从atom得到的下一个资源链接的概念。我实现的解决方案是向我发送的json映射中添加“下一个”和“上一个”键,然后就完成了。之后我开始思考可能要做的事情是发送307 - 临时重定向到一个链接,例如/db/questions/1,25 - 这样可以将原始URI留作规范资源名称,但它会让你进入一个名字恰当的子资源。这是我想从413中看到的行为,但307似乎是一个很好的妥协。虽然我还没有在代码中实际尝试过这种方法。更好的是重定向到包含最近提问问题的实际ID的URL。例如,如果每个问题都有一个整数ID,并且系统中有100个问题,您想显示最近的十个问题,则对/db/questions的请求应该被307重定向到/db/questions/100,91。这是一个非常好的问题,谢谢你提出它。你为我确认了我花费几天时间思考它并不是疯了。

303比307在这方面更好。307意味着原始URL将很快开始按照客户端的预期进行响应。 - Nicholas Shanks
RFC 7231将HTTP状态码413称为“负载过大”,并将此代码与请求大小而非可能的响应大小相关联。 - MÇT

2

2

范围头的一个大问题是许多企业代理会过滤它们。我建议使用查询参数代替。


1
你可以检测Range头,如果存在则模仿Dojo,如果不存在则模仿Atom。在我看来,这样可以很好地划分使用情况。如果您正在响应应用程序的REST查询,则希望它使用Range头格式化。如果您正在响应休闲浏览器,则如果返回分页链接,它将让工具提供一种轻松的方式来浏览集合。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接