休息集合中的分页


134

我有兴趣向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中使用Range与自定义范围单位一起使用标头的RFC2616兼容扩展,解决了此问题items。结果是206 Partial Content仅返回要求范围的。与查询参数相比,此方法的优势在于,它将查询字符串留给...查询(例如GET /db/questions/?score>200,诸如此类,是的,将被编码%3E)。

这种方法完全涵盖了我想要的行为。问题在于RFC 2616在206响应上指定了此内容(重点是我的):

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

这在标头的标准用法的上下文中是有道理的,但是这是一个问题,因为我希望206响应是默认值,以处理幼稚的客户/随机人员进行探索。

我已经详细研究了RFC,以寻找解决方案,但对我的解决方案不满意,并且对SO解决该问题很感兴趣。

我的想法:

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

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

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


21
哇,这是一个很好的例子,这个问题以前已经做过认真的思考。
Heiko Rupp


1
至于Dojo使用Range标头的方法,尽管Accept-Ranges允许扩展,但据我所知,Range的EBNF不允许:tools.ietf.org/html/rfc2616#section-14.35.2。规范指出Range = "Range" ":" ranges-specifiertools.ietf.org/html/rfc2616#section-14.35.1中后者的位置仅描述为“ byte-ranges-specifier”,其必须以“ bytes-unit”开头,并定义为字符串“ bytes” ”。
Brett Zamir

2
Content-Range报头适用于所述主体(可以与请求上载大文件等时下载时使用,或用于响应)。所述Range报头被用来请求在一定的范围。206Range标头包含在请求中时,应该做出回应。如果不是,则响应可能仍包含Content-Range标头,但响应代码应为200。实际上,此标头似乎是分页的理想选择。
Stijn de Witt'5

但是RFC 2616本身说“ HTTP / 1.1实现可以忽略使用其他单元指定的范围”。那么使用Range标头进行分页是一种好习惯吗?因为它可能会损害互操作性。
chetan choulwar

Answers:


23

我的直觉是,HTTP范围扩展不是针对您的用例设计的,因此您不应该尝试。部分响应暗含206206只有在客户要求时才必须发送。

您可能需要考虑另一种方法,例如Atom中的一种用法(在这种情况下,按设计表示可能是部分表示,并且返回带有status的表示200,并可能是分页链接)。请参阅RFC 4287RFC 5005


14
Dojo的用法完全在规范范围内。如果服务器不了解items范围单位,它将返回完整的响应。我熟悉Atom,但这不是Rest分页的通用解决方案。这不是针对单个案例的解决方案,而是更多一般解决方案。并非所有文档/集合都适合Atom模型,除非有必要,否则没有理由强制使用它。
Karl Guertin

1
@KarlGuertin同意。遗憾的是,这是一个可以接受的答案,因为似乎社区中的许多人实际上正在拥抱RangeContent-Range出于分页的目的。
Stijn de Witt

34

我真的不同意你们中的一些人。我已经为我的REST服务使用了数周的功能。我最终所做的事情非常简单。我的解决方案仅对REST人员所说的集合有意义。

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

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

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

我将ETag / If-Match与Last-Modified / If-Unmodified-Since一起使用来优化缓存。浏览器和代理可能依赖它们中的一个或两者来作为缓存算法。

我认为,除非包含搜索/过滤器查询,否则URL应该干净。如果您考虑一下,搜索只不过是集合的部分视图。而不是car / search?q = BMW类型的URL,我们应该看到更多的cars?manufacturer = BMW。


您是说416“无法满足请求的范围”还是“ 413”请求实体太大?

1
@Mohamed我想您的意思是If-Unmodified-Since,它对应于E-Tag变体If-Match,而不是If-Modified-Since。也就是说,您还可以根据使用情况考虑删除此约束。假设您有一个仅从顶部开始增长的集合(例如某些“最新的第一”样式集合),则如果该集合在请求之间发生变化,则可能发生的最糟糕的情况是,浏览该集合的用户两次看到条目。(其本身也是有用的信息:它告诉用户集合已更改)
Eugene Beresovsky 2013年

20
413是“请求实体太大”,而不是“请求实体太大”。这意味着您的请求大小(例如,在上传文件时)大于服务器愿意处理的大小。因此,将其用于此似乎并不完全合适。
user247702

@Mohamed我知道这是一个老问题,但是如果集合的ETag是该集合包含的最近修改资源的ETag,那么在修改集合中的一个资源时应使用If-Match标头的哪个值?使用由集合返回的ETag的值是错误的,因为即使客户端没有看到资源的最后状态,它也可以修改资源。
Mickael Marrache

8
我强烈反对使用413。这是一个错误代码,表示客户端发送的邮件由于大小原因服务器拒绝接受。并非相反!请参阅tools.ietf.org/html/rfc7231#section-6.5.11(请注意,它表示请求有效负载,而不是响应有效负载)!
2015年

7

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

我会用Range的分页,并有它只是返回200一个普通的GET

感觉100%RESTful 不会使浏览变得更加困难。

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


5

如果回答不只一页,并且您不想一次提供整个收藏集,这是否意味着有多个选择?

在一个请求/db/questions,回300 Multiple ChoicesLink头指定如何去每个页面以及一个JSON对象或HTML页面的URL列表。

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可处理基于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撰写的文章中整理笔记(无法链接到这里,我将其列出在我的个人资料中),您可能会非常明确,并在此处提供规范/相关版本,以防您需要重新定义page参数的含义在将来。


1
+1链接标头,但我还建议使用通用的first,prev,next,last rels,以及RFC5005的prev-archive,next-archive和current。
约瑟夫·霍尔斯滕

> 在对/ db / questions的请求中,返回300个带有链接头的“多项选择”,这些头指定了如何进入每个页面 。目标是最大程度地减少网络请求。第一个请求应该产生结果,而不是链接到更多最终将提供我们所需数据的请求。
Stijn de Witt'5

4

编辑:

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

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

因此,基本上,我重申了关于使用标题的原始答案(如下)。


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

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

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

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

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


感谢您的回复。我已经考虑过这个话题,但是希望能得到第二意见。碰巧有标题参数的指针?
Karl Guertin

这是我唯一已添加书签的网站(请参阅评论中的讨论):barlylyenough.org/blog/2008/05/versioning-rest-web-services 另一个网站围绕Ruby使用.json,.xml和.what请求的内容类型。一些示例:*语言-将其放在URL中意味着将链接发送到另一个国家/地区将以错误的语言显示它。*分页-将其放在页眉中意味着您无法将人们链接到所看到的内容
Richard Levasseur 2009年

*内容类型:语言和分页问题的组合-如果它在url中,如果客户端不支持该内容类型(例如.ajax和.html扩展名),该怎么办?相反,如果没有url中的内容类型,则无法确保给出相同的表示形式。“新的ajax网站!example.com/cool.ajax”与“此处的酷文章:example.com/article.ajax#id=123”。
理查德·勒瓦瑟尔

2
IMO,是否在URL中取决于它是什么。我的一般规则是,如果它将标识一个具体的资源(无论是处于特定状态的资源,资源的选择还是离散的结果),它就会进入URL。搜索查询,分页和静态事务就是很好的例子。如果需要将抽象表示转换为具体表示的东西,则将其放在标头中。auth info和content-type是很好的例子。
理查德·勒瓦瑟尔

我认为URL中的查询字符串是用于查询指定资源的选项。
wprl

3

您可能会考虑使用像Atom Feed协议之类的模型,因为它具有合理的HTTP模型集合以及如何操作它们(“疯狂”表示WebDAV)。

Atom发布协议,它定义了收集模型和REST操作,此外您还可以使用RFC 5005-Feed分页和归档来翻阅大集合。

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


3

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

最近,我在同一个问题上挣扎,我在RESTful Web服务中寻找灵感书中。我个人认为206由于标题要求而不合适。我的想法也使我达到了300,但是我认为这更多地适用于不同的mime类型,因此我查阅了Richardson和Ruby在第377页附录B中关于该主题的看法。他们建议服务器只选择首选的表示形式,然后将其发送回200,基本上忽略了应该是300的概念。

这也与原子到我们拥有的下一个资源的链接的概念有关。我实现的解决方案是将“ next”和“ previous”键添加到要发送回的json映射中,并完成此操作。

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

这是一个很好的问题,谢谢提出。您为我确认,我花了几天的时间思考这个并不是很疯狂。


在这方面,303比307更好。307意味着原始URL将很快开始按照客户端的期望进行响应。
尼古拉斯·香克斯

RFC 7231将HTTP状态代码413称为有效载荷过大,并将此代码与请求大小而非潜在响应大小相关联。
beawolf

1

您可以检测到 Range标头,如果Dojo存在,则模仿它;如果不存在,则模仿Atom。在我看来,这很好地划分了用例。如果您正在响应来自应用程序的REST查询,则希望它使用Range标题格式化。如果您响应的是休闲浏览器,那么如果您返回分页链接,它将使该工具提供一种轻松的方法来浏览集合。


1

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



0

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


0

尽管可以将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参数中添加其他内容。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.