我有兴趣将直接REST接口暴露给JSON文档集合(想想CouchDB或Persevere)。我遇到的问题是如果集合很大,如何处理集合根上的GET
操作。
作为一个例子假装我暴露了StackOverflow的Questions
表,其中每一行都作为文档公开(不一定是这样的表,只是一个相当大的'文档'集合的具体例子)。该集合将在/db/questions
处提供,通常的CRUD api GET /db/questions/XXX
,PUT /db/questions/XXX
,POST /db/questions
正在播放中。获取整个集合的标准方法是GET /db/questions
,但如果天真地将每一行转储为JSON对象,那么您将获得相当大的下载和服务器上的大量工作。
解决方案当然是分页。 Dojo通过一个聪明的RFC2616兼容扩展程序在JsonRestStore中解决了这个问题,该扩展使用Range
标头和自定义范围单元items
。结果是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
! - 我不认为这是错误的,但我更喜欢一个更明显的指标,即响应是只有部分内容。400 Range Required
- 没有针对所需标头的特殊400响应代码,因此必须手动使用和读取默认错误。这也使得通过Web浏览器(或像Resty这样的其他客户端)进行探索变得更加困难。206
! - 我认为大多数客户都不会惊慌失措,但我宁愿不反对RFC中的必须266 Partial Content
- 行为与206完全相同,但是响应的请求不得包含Range
标头。我认为266足够高,我不应该遇到碰撞问题,这对我来说是有意义的,但我不清楚这是否被认为是禁忌。我认为这是一个相当普遍的问题,我希望以某种事实上的方式看待这一点,所以我或其他人不会重新发明轮子。
当集合很大时,通过HTTP公开完整集合的最佳方法是什么?
答案 0 :(得分:33)
我真的不同意你们中的一些人。我已经为我的REST服务工作了几个星期。我最终做的很简单。我的解决方案只对REST人称之为集合的内容有所了解。
客户端必须包含一个“范围”标题,以指示他所需的集合的哪个部分,或者当请求的集合太大而无法在单个往返中检索时准备好处理413请求的实体太大错误
服务器发送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应该是干净的,除非它包含搜索/过滤查询。如果你考虑一下,搜索只不过是一个集合的局部视图。而不是汽车/搜索?q =宝马类型的URL,我们应该看到更多的汽车?制造商=宝马。
答案 1 :(得分:22)
我的直觉是HTTP范围扩展不是为您的用例而设计的,因此您不应该尝试。部分回复意味着206
,并且只有在客户要求时才会发送206
。
您可能需要考虑一种不同的方法,例如在Atom中使用的方法(设计中的表示可能是部分的,并且返回状态为200
,并且可能是分页链接)。请参阅RFC 4287和RFC 5005。
答案 2 :(得分:5)
如果有多页回复,并且您不想一次提供整个收藏,那是否意味着有多种选择?
在对/db/questions
的请求中,返回带有300 Multiple Choices
标题的Link
,其中指定了如何访问每个页面以及带有URL列表的JSON对象或HTML页面。< / p>
Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...
每个结果页面都有一个Link
标题(空字符串表示当前URL,每个页面的URL相同,只是使用不同的范围访问),并且定义了关系为a custom one per the upcoming Link
spec。这种关系可以解释您的自定义266
或违反206
的行为。这些标题是您的机器可读版本,因为您的所有示例都需要了解客户端。
(如果您坚持使用“范围”路线,我相信您自己的2xx
返回代码,正如您所描述的那样,将是此处的最佳行为。您应该为您的应用程序执行此操作[“HTTP状态代码是可扩展的。”],你有充分的理由。)
300 Multiple Choices
说你还应该为机构提供一种让用户代理选择的方式。如果您的客户理解,则应使用Link
标头。如果是用户手动浏览,可能是一个HTML页面,其中包含指向特殊“分页”根资源的链接,该资源可以根据URL处理该特定页面的呈现? /humanpage/1/db/questions
还是像那样可怕的东西?
对Richard Levasseur的帖子的评论提醒我一个额外的选项:Accept
标题(第14.1节)。回到oEmbed规范出来的时候,我想知道为什么没有完全使用HTTP,并用它们编写了一个替代方案。
保留初始天真HTTP 300 Multiple Choices
的{{1}},Link
标题和HTML页面,但是不要使用范围,请让新的分页关系定义{的使用{1}}标题。您的后续HTTP请求可能如下所示:
GET
Accept
标题允许您定义可接受的内容类型(您的JSON返回),以及该类型的可扩展参数(您的页码)。从我的oEmbed写的重复我的笔记(这里不能链接到它,我会在我的个人资料中列出),你可以非常明确地提供一个规范/关系版本,以防你需要重新定义{{{ 1}}参数意味着将来。
答案 3 :(得分:5)
您仍然可以使用Accept-Ranges
回复代码返回Content-Ranges
和200
。这两个响应标头为您提供了足够的信息,以推断 206
响应代码明确提供的相同信息。
我会使用Range
进行分页,只需为普通200
返回GET
。
这感觉100%RESTful 和不会让浏览变得更加困难。
编辑: 我写了一篇关于此的博文:http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
答案 4 :(得分:4)
编辑:
在考虑了一点之后,我倾向于同意Range标题不适合分页。逻辑是,Range标头用于服务器的响应,而不是应用程序。如果您提供了100兆字节的结果,但服务器(或客户端)一次只能处理1兆字节,那就是Range标头的用途。
我也认为资源的一个子集是它自己的资源(类似于关系代数)。所以它应该在URL中表示。
所以基本上,我放弃了关于使用标题的原始答案(下面)。
我认为您或多或少地回答了您自己的问题 - 使用内容范围返回200或206,并可选择使用查询参数。我会嗅探用户代理和内容类型,并根据这些检查查询参数。否则,需要范围标题。
你本质上有相互冲突的目标 - 让人们使用他们的浏览器进行探索(不容易自定义标题),或者强迫人们使用可以设置标题的特殊客户端(这不会让他们探索)。 / p>
您可以根据请求向他们提供特殊客户端 - 如果它看起来像普通浏览器,请发送一个小的ajax应用程序来呈现页面并设置必要的标题。
当然,还有关于URL是否应包含此类事物的所有必要状态的争论。使用标题指定范围可被某些人视为“不安宁”。
顺便说一句,如果服务器可以使用“Can-Specify:Header1,header2”标头进行响应,并且Web浏览器会显示一个UI,以便用户可以根据需要填写值,那就太好了。
答案 5 :(得分:3)
你可能会考虑使用像Atom Feed Protocol这样的模型,因为它有一个合理的HTTP模型集合以及如何操作它们(疯狂意味着WebDAV)。
Atom Publishing Protocol定义了集合模型和REST操作,您还可以使用RFC 5005 - Feed Paging and Archiving来浏览大型集合。
从Atom XML切换到JSON内容不应影响这个想法。
答案 6 :(得分:3)
我认为这里真正的问题是规范中没有任何内容告诉我们在面对413时请求如何进行自动重定向 - 请求的实体太大。
我最近遇到了同样的问题,我在 RESTful Web Services 一书中寻找灵感。就个人而言,由于标题要求,我认为206不合适。我的想法也让我达到了300,但我认为对于不同的mime类型更多,所以我查看了Richardson和Ruby在附录B,第377页中对该主题的看法。他们建议服务器选择首选表示并用200发回,基本上忽略了它应该是300的概念。
这也与我们从原子获得的下一个资源的链接概念相吻合。我实现的解决方案是将“next”和“previous”键添加到我发回的json地图中并完成它。
后来我开始考虑可能要做的事情就是将307 - 临时重定向发送到类似于/ db / questions / 1,25的链接 - 将原始URI作为规范资源名称,但它将您带到适当命名的下级资源。这是我想从413中看到的行为,但307似乎是一个很好的妥协。尽管如此,还没有在代码中尝试过这个。更好的方法是将重定向重定向到包含最近提问的实际ID的URL。例如,如果每个问题都有一个整数ID,并且系统中有100个问题,并且您希望显示最近的十个问题,那么对/ db / questions的请求应该是307 / to / db / questions / 100,91
这是一个非常好的问题,谢谢你的提问。你向我证实,我花了几天时间思考它并不是疯了。
答案 7 :(得分:1)
您可以检测Range
标头,并模拟Dojo(如果存在),并模仿Atom(如果不存在)。在我看来,这巧妙地划分了用例。如果您正在响应应用程序中的REST查询,则希望使用Range
标头对其进行格式化。如果您对休闲浏览器做出响应,那么如果您返回分页链接,它将让该工具提供一种简单的方法来探索该集合。
答案 8 :(得分:1)
随着rfc723x的发布,未注册的范围单元确实违反了规范中的明确建议。考虑rfc7233(弃用rfc2616):
“New range units ought to be registered with IANA”(以及对HTTP Range Unit Registry的引用)。
答案 9 :(得分:1)
范围标题的一个大问题是许多公司代理会过滤掉它们。我建议改用查询参数。
答案 10 :(得分:0)
在我看来,最好的方法是将范围包含为查询参数。例如, GET / db / questions /?date&gt; mindate&amp; date&lt; maxdate 。在没有查询参数的GET到/ db / questions /后,返回303,其中位置:/ db / questions /?query-parameters-to-retrieve-the-default-page 。然后提供一个不同的URL,消费者通过该URL获取有关集合的统计信息(例如,如果他/她想要整个集合,将使用哪些查询参数);
答案 11 :(得分:0)
虽然可以将Range标题用于此目的,但我并不认为这是意图。它似乎是为处理片状连接以及限制数据而设计的(因此如果缺少某些内容或者大小太大而无法处理,客户端可以请求部分请求)。您正在将分页破解成可能在通信层用于其他目的的内容。 &#34;适当&#34;处理分页的方法是返回的类型。而不是返回问题对象,而应该返回一个新类型。
如果问题是这样的话:
<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>
当然,您可以控制您的媒体类型,这样您就可以制作自己的媒体类型&#34;一种适合您需求的格式。如果你使make是通用的,你可以在客户端上有一个解析器来处理所有类型的分页。我认为这更符合HTTP规范的精神,而不是为其他东西捏造Range参数。