【问题标题】:Paging in a Rest Collection休息集合中的分页
【发布时间】:2010-10-29 19:06:55
【问题描述】:

我有兴趣向 JSON 文档集合公开直接 REST 接口(想想CouchDBPersevere)。我遇到的问题是如果集合很大,如何处理集合根上的GET 操作。

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

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

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

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

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

我已经详细阅读了 RFC 以寻找解决方案,但对我的解决方案不满意,并且对 SO 对这个问题的看法很感兴趣。

我的想法:

  • 返回 200 并带有 Content-Range 标头! - 我不认为这是错误的,但我更喜欢更明显的指示,表明响应只是部分内容。
  • Return 400 Range Required - 对于必需的标头没有特殊的 400 响应代码,因此必须使用默认错误并手动读取。这也使得通过网络浏览器(或其他一些客户端,如 Resty)进行探索变得更加困难。
  • 使用查询参数 - 标准方法,但我希望允许查询坚持不懈,这会切入查询命名空间。
  • 只需返回 206 - 我认为大多数客户不会惊慌失措,但我宁愿不违反 RFC 中的 MUST
  • 扩展规范!返回 266 Partial Content - 行为与 206 完全相同,但响应的请求不得包含 Range 标头。我认为 266 足够高,我不应该遇到碰撞问题,这对我来说很有意义,但我不清楚这是否被视为禁忌。

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

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

【问题讨论】:

  • 哇,这是一个很好的例子,说明之前已经进行了一些认真思考的问题。
  • 就 Dojo 使用 Range 标头的方法而言,尽管 Accept-Ranges 允许扩展,但据我所知,Range 的 EBNF 不允许:tools.ietf.org/html/rfc2616#section-14.35.2。规范指出Range = "Range" ":" ranges-specifier,其中tools.ietf.org/html/rfc2616#section-14.35.1 中的后者仅被描述为“byte-ranges-specifier”,它必须以“bytes-unit”开头,定义为字符串“bytes”。
  • Content-Range 标头适用于正文(可用于上传大文件等时的请求,或下载时的响应)。 Range 标头用于请求某个范围。当请求中包含Range 标头时,应使用206 响应。如果不是,则响应可能仍包含Content-Range 标头,但响应代码应为200。这个标头实际上似乎非常适合分页。
  • 但是 RFC 2616 本身说“HTTP/1.1 实现可以忽略使用其他单位指定的范围。”那么使用 Range 标头进行分页是一种好习惯吗?因为它可能会损害互操作性。

标签: http rest http-headers pagination


【解决方案1】:

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

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

【讨论】:

  • Dojo 的使用完全符合规范。如果服务器不理解items 范围单元,它会返回一个完整的响应。我对 Atom 很熟悉,但这不是 Rest 分页的一般解决方案。这不是单一案例的解决方案,更多的是一般解决方案应该是什么。并非所有文档/集合都适合 Atom 模型,除非需要,否则没有理由强制它。
  • @KarlGuertin 同意。太糟糕了,这是公认的答案,因为似乎社区中的许多人实际上都在接受 RangeContent-Range 用于分页目的。
【解决方案2】:

您可能会考虑使用类似于 Atom Feed 协议的模型,因为它具有健全的 HTTP 集合模型以及如何操作它们(其中疯狂意味着 WebDAV)。

Atom Publishing Protocol 定义了集合模型和 REST 操作,此外,您还可以使用 RFC 5005 - Feed Paging and Archiving 对大型集合进行分页。

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

【讨论】:

    【解决方案3】:

    编辑:

    再想一想之后,我倾向于同意 Range 标头不适合分页。逻辑是, Range 标头用于服务器的响应,而不是应用程序。如果您提供 100 兆字节的结果,但服务器(或客户端)一次只能处理 1 兆字节,那么这就是 Range 标头的用途。

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

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


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

    您的目标本质上是相互矛盾的 - 让人们使用他们的浏览器进行探索(这不容易允许自定义标头),或者强迫人们使用可以设置标头的特殊客户端(不允许他们探索)。

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

    当然,还有关于 URL 是否应该包含此类事情的所有必要状态的争论。使用标题指定范围可能被某些人认为是“不安的”。

    顺便说一句,如果服务器可以响应“Can-Specify: Header1, header2”标头,并且 Web 浏览器会呈现一个 UI 以便用户可以根据需要填写值,那就太好了。

    【讨论】:

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

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

    【讨论】:

      【解决方案5】:

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

      在对/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 相同,只是访问范围不同),关系定义为 @ 987654321@。这种关系将解释您的自定义266,或您对206 的违反。这些标头是您的机器可读版本,因为无论如何您的所有示例都需要一个理解客户端。

      (如果您坚持“范围”路线,我相信您自己的 2xx 返回代码,正如您所描述的,将是这里的最佳行为。您应该为您的应用程序和此类 [" HTTP 状态码是可扩展的。”],你有充分的理由。)

      300 Multiple Choices 说您还应该为主体提供一种供用户代理选择的方式。如果您的客户理解,它应该使用Link 标头。如果是用户手动浏览,可能是一个带有指向特殊“分页”根资源的链接的 HTML 页面,该根资源可以处理基于 URL 呈现该特定页面? /humanpage/1/db/questions 或类似的可怕的东西?


      Richard Levasseur 帖子上的 cmets 让我想起了另一个选项: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 文章中翻阅我的笔记(无法在此处链接,我将在我的个人资料中列出),您可以非常明确并在此处提供规范/关系版本,以防您需要重新定义 @987654342 的内容@参数表示未来。

      【讨论】:

      • +1 链接头,但我也推荐常见的 first、prev、next、last rels,以及 RFC5005 的 prev-archive、next-archive 和 current。
      • > 在对 /db/questions 的请求中,返回 300 个带有指定如何访问每个页面的链接标头的多项选择 [..]与大多数纯 REST 设计)是它正在杀死延迟。目标是最小化网络请求。第一个请求应该产生结果,而不是链接到最终会提供我们需要的数据的更多请求。
      • 您也可以查看 Google 为 Android 创建的示例github.com/android/architecture-components-samples/blob/main/… 查看 ApiSuccessResponse 类
      【解决方案6】:

      我认为这里真正的问题是规范中没有任何内容告诉我们在面对 413 - Requested Entity Too Large 时如何进行自动重定向。

      我最近也在为同样的问题苦苦挣扎,我在 RESTful Web Services 一书中寻找灵感。由于标题要求,我个人认为 206 不合适。我的想法也将我带到了 300,但我认为对于不同的 mime 类型来说更多,所以我在附录 B,第 377 页中查找了 Richardson 和 Ruby 就这个主题所说的话。他们建议服务器只选择首选表示并将其发送回 200,基本上忽略了它应该是 300 的概念。

      这也与指向我们从 atom 获得的下一个资源的链接的概念相吻合。我实施的解决方案是在我发回的 json 映射中添加“下一个”和“上一个”键并完成它。

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

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

      【讨论】:

      • 303 在这方面会比 307 更好。307 意味着原始 URL 将很快开始响应客户端的期望。
      • RFC 7231 将 HTTP 状态代码 413 引用为 Payload Too Large 并将此代码与请求大小而非潜在响应大小相关联。
      【解决方案7】:

      在我看来,最好的方法是将范围作为查询参数。例如,GET /db/questions/?date>mindate&date。在没有查询参数的情况下对 /db/questions/ 进行 GET,返回 303 并带有 Location: /db/questions/?query-parameters-to-retrieve-the-default-page。然后提供一个不同的 URL,由使用您的 API 的人获取有关集合的统计信息(例如,如果她/他想要整个集合,则使用哪些查询参数);

      【讨论】:

        【解决方案8】:

        我真的不同意你们中的一些人。我已经为我的 REST 服务的这个特性工作了几个星期。我最终做的事情真的很简单。我的解决方案只对 REST 人所说的集合有意义。

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

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

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

        我将 ETag/If-Match 与 Last-Modified/If-Unmodified-Since 结合使用来优化缓存。浏览器和代理可能依赖其中之一或两者来实现缓存算法。

        我认为 URL 应该是干净的,除非它包含搜索/过滤查询。如果你仔细想想,搜索只不过是一个集合的局部视图。我们应该看到更多的cars?manufacturer=BMW,而不是cars/search?q=BMW 类型的URL。

        【讨论】:

        • 您的意思是 416“请求的范围不可满足”还是“413”请求实体太大?
        • @Mohamed 我想你的意思是If-Unmodified-Since,它对应于电子标签变体If-Match,而不是If-Modified-Since。也就是说,您也可以考虑删除此约束,具体取决于您的用例。假设您有一个仅从顶部增长的集合(例如某些“最新的优先”样式集合),如果该集合在请求之间发生更改,则可能发生的最糟糕的情况是,翻阅集合的用户会看到两次条目。 (这本身也是一个有用的信息:它告诉用户集合发生了变化)
        • 413 是“请求实体太大”,而不是“请求实体太大”。这意味着您的请求的大小(例如在上传文件时)大于服务器愿意处理的大小。所以用它来做这个似乎并不完全合适。
        • @Mohamed 我知道这是一个老问题,但是如果集合的 ETag 是集合包含的最近修改资源的 ETag,那么在修改一个时应该使用 If-Match 标头的哪个值集合中的资源?使用集合返回的 ETag 的值是错误的,因为即使客户端没有看到资源的最后状态,他也可以修改资源。
        • 我强烈反对使用413。这是一个错误代码,表示客户端发送服务器因大小而拒绝接受的内容。不是反过来!见tools.ietf.org/html/rfc7231#section-6.5.11(注意它说的是request有效载荷。不是response有效载荷)!
        【解决方案9】:

        您仍然可以使用200 响应代码返回Accept-RangesContent-Ranges。这两个响应标头为您提供了足够的信息来推断206 响应代码明确提供的相同信息。

        我会使用Range 进行分页,并让它简单地返回一个200 来表示一个普通的GET

        这感觉是 100% RESTful 并且不会让浏览变得更加困难。

        编辑: 我为此写了一篇博文:http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

        【讨论】:

        • 你如何用这种方法沟通下一部分。如果分页参数是查询的一部分,则很容易告诉消费者元信息,例如下一页和最后一页。就像在这里看到的docs.github.com/en/rest/guides/…
        【解决方案10】:

        随着rfc723x 的发布,未注册的范围单位确实违反了规范中的明确建议。考虑rfc7233(弃用 rfc2616):

        New range units ought to be registered with IANA”(以及对 HTTP Range Unit Registry 的引用)。

        【讨论】:

          【解决方案11】:

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

          【讨论】:

            【解决方案12】:

            虽然可以为此目的使用 Range 标头,但我认为这不是目的。它似乎是为处理不稳定的连接以及限制数据而设计的(因此,如果缺少某些内容或大小太大而无法处理,客户端可以请求部分请求)。您正在将分页侵入可能在通信层用于其他目的的东西。 处理分页的“正确”方法是使用您返回的类型。与其返回问题对象,不如返回一个新类型。

            所以如果问题是这样的:

            <questions> <question index=1></question> <question index=2></question> ... </questions>

            新类型可能是这样的:

            <questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

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

            【讨论】:

              猜你喜欢
              • 2017-09-24
              • 1970-01-01
              • 2011-12-17
              • 2011-03-26
              • 2011-11-07
              • 2015-11-12
              • 2019-04-14
              • 1970-01-01
              • 2019-07-20
              相关资源
              最近更新 更多