在restcollections中寻呼
我有兴趣将直接的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对象转储,那么在服务器上你将得到相当大的下载和很多工作。
解决scheme当然是分页。 Dojo已经在其JsonRestStore中解决了这个问题,通过使用Range
标头和自定义范围单元items
的聪明的符合RFC2616的扩展。 结果是206 Partial Content
仅返回请求的范围。 这种方法在查询参数上的优点是,它留下了查询的查询string(例如, GET /db/questions/?score>200
或其他,并且是编码%3E
)。
这种方法完全覆盖了我想要的行为。 问题是, RFC 2616指出,在206响应(重点矿):
该请求必须包含一个Range头域( 14.35节 ),表示期望的范围,并且可以包含一个If-Range头域( 14.27节 )来使请求有条件。
这在标题的标准用法的上下文中是有意义的,但这是一个问题,因为我想将206响应作为默认处理初始客户端/随机人员的探索。
我已经详细了解RFC,寻找解决scheme,但是对我的解决scheme感到不满,并且对SO的解决scheme感兴趣。
我有过的想法:
- 用
Content-Range
头返回200
! – 我不认为这是错误的,但我更喜欢如果一个更明显的指标,反应是只有部分内容。 - 返回
400 Range Required
– 对于所需的头文件没有特殊的400响应代码,所以必须使用和手动读取默认错误。 这也使得通过网页浏览器(或Resty等其他客户端)更难探索。 - 使用一个查询参数 – 标准的方法,但我希望允许查询la Persevere,这削减到查询命名空间。
- 只要返回
206
! – 我认为大多数客户不会被吓倒,但是我不想在RFC中反对 - 扩展规范! 返回
266 Partial Content
– 完全像206一样,但是是响应一个不能包含Range
标头的请求。 我认为266是足够高,我不应该碰到碰撞问题,这对我来说是有道理的,但我不清楚这是否被视为禁忌。
我认为这是一个相当普遍的问题,我希望看到这种事实上是以某种事实的方式完成的,所以我或其他人没有重新发明轮子。
当集合很大时,通过HTTP公开完整集合的最佳方式是什么?
我的直觉是,HTTP范围扩展不是为你的用例devise的,因此你不应该尝试。 部分响应意味着206
,只有当客户端要求时才能发送206
。
您可能需要考虑一种不同的方法,例如Atom中的一种方法(其中devise的表示可能是部分的,并且以状态200
返回,并且可能包含分页链接)。 请参阅RFC 4287和RFC 5005 。
我不是很同意你们中的一些人。 我已经为我的REST服务工作了几个星期。 我最终做的事情非常简单。 我的解决scheme只能说明REST人员称为集合的意义。
客户端必须包含一个“范围”头,以指示他需要的集合中的哪一部分,或者当请求的集合太大而不能在单个往返中检索时,准备处理请求的实体太大的错误。
服务器发送一个206 PARTIAL CONTENT响应,Content-Range报头指定资源的哪一部分已经发送,一个ETag报头用于标识集合的当前版本。 我通常使用类似Facebook的ETag {last_modification_timestamp} – {resource_id},我认为集合的ETag是它包含的最近修改过的资源的ETag。
为了请求一个集合的特定部分,客户端必须使用“Range”头部,并将“If-Match”头部填充到从先前执行的请求获得的集合的ETag中,以获取同一集合的其他部分。 服务器因此可以在发送所请求的部分之前validation集合没有改变。 如果存在更新版本,则会返回412 PRECONDITION FAILED响应,邀请客户端从头开始检索集合。 这是必要的,因为这可能意味着某些资源可能在当前请求的部分之前或之后被添加或删除。
我使用ETag / If-Match与Last-Modified / If-Unmodified-Since来优化caching。 浏览器和代理可能依靠它们中的一个或两个用于它们的cachingalgorithm。
我认为一个URL应该是干净的,除非它包含一个search/filter查询。 如果你仔细想想,search只不过是一个集合的局部视图。 而不是汽车/search?q =宝马types的URL,我们应该看到更多的汽车?制造商=宝马。
如果有超过一页的回复,而且你不想一次提供整个系列,这是否意味着有多种select?
在对/db/questions
的请求中,返回300 Multiple Choices
具有Link
标题的300 Multiple Choices
,指定如何进入每个页面以及具有URL列表的JSON对象或HTML页面。
Link: <>; rel="http://paged.collection.example/relation/paged" Link: <>; rel="http://paged.collection.example/relation/paged" ...
每个结果页面都有一个Link
标题(一个空string表示当前的URL,每个页面的URL是相同的,只是用不同的范围访问),并且关系被定义为每个即将到来的一个Link
规范 。 这种关系可以解释你的习惯266
,或者你违反了206
。 这些头文件是您的机器可读版本,因为您的所有示例都需要理解客户端。
(如果你坚持使用“范围”路由,我相信你自己的2xx
返回代码,就像你描述的那样,这将是最好的行为,你应该为你的应用程序做这些事情,这样的[“HTTP状态代码是可扩展的。“,你有很好的理由。)
300 Multiple Choices
,你应该也提供一个方法供用户代理挑选。 如果你的客户是理解,它应该使用Link
标题。 如果是用户手动浏览,也许是一个HTML页面,可以链接到一个特殊的“分页”根资源,可以处理基于URL呈现特定的页面? /humanpage/1/db/questions
或类似的可怕吗?
Richard Levasseur的post上的评论让我想起了一个额外的选项: Accept
头(14.1节)。 当oEmbed规范出来的时候,我想知道为什么没有完全使用HTTP,并且写了一个使用它们的替代scheme。
保持300 Multiple Choices
, 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
头允许你定义一个可接受的内容types(你的JSON返回值),加上这个types的扩展参数(你的页面号)。 从我的oEmbed文档(无法链接到这里,我会列出它在我的configuration文件中),我可以非常明确地提供一个规格/关系版本,以防万一需要重新定义page
参数意味着未来。
您仍然可以使用200
响应代码返回Accept-Ranges
和Content-Ranges
。 这两个响应标题为您提供足够的信息来推断 206
响应代码明确提供的相同信息。
我会使用Range
分页,并只是返回一个简单的GET
200
。
这感觉100%RESTful ,并没有使浏览更困难。
编辑:我写了一篇博客文章: http : //otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
你可能会考虑使用类似Atom Feed Protocol的模型,因为它有一个合理的HTTP模型,以及如何操纵它们(疯狂的意思是WebDAV)。
Atom发布协议定义了集合模型和REST操作,并且您可以使用RFC 5005 – “Feed分页和归档”通过大集合进行分页 。
从Atom XML切换到JSON内容不应该影响这个想法。
编辑:
想了一会儿之后,我倾向于同意范围标题不适合分页。 逻辑是,Range头部是用于服务器的响应,而不是应用程序。 如果服务器的结果是100兆字节,但是服务器(或客户端)一次只能处理1兆字节,那么这就是Range标头的用途。
我也认为,资源的一个子集是自己的资源(类似于关系代数),所以它值得在URL中的表示。
所以基本上,我收回我原来的答案(下面)有关使用标题。
我认为你回答了你自己的问题,或多或less地返回200或206与内容范围和可选地使用查询参数。 我会嗅探用户代理和内容types,并根据这些,检查查询参数。 否则,需要范围标题。
实际上,您的目标有冲突 – 让人们使用浏览器来探索(不容易自定义标题),或者强迫人们使用可以设置标题的特殊客户端(这不允许他们探索)。
你可以根据请求为他们提供特殊的客户端 – 如果它看起来像一个普通的浏览器,发送一个小的Ajax应用程序呈现页面,并设置必要的标题。
当然,也有关于这个URL是否应该包含所有必要的状态的争论。 指定范围使用标题可以被认为是“不安分”的一些。
另外,如果服务器可以用“Can-Specify:Header1,header2”头来响应,Web浏览器将呈现一个用户界面,用户可以根据需要填写值。
我认为这里真正的问题在于规范中没有任何内容告诉我们如何在遇到413-要求实体太大时自动redirect。
最近我也在为这个问题苦苦挣扎,我在REST风格的Web服务书中寻找灵感。 就我个人而言,由于标题要求,我不认为206是合适的。 我的想法也使我达到了300,但是我认为这是针对不同的mimetypes的,所以我查了Richardson和Ruby在附录B第377页上对这个主题所说的话。他们build议服务器只select首选代表和发回200,基本上忽略了它应该是一个300的概念。
这也与我们从primefaces中获得的下一个资源链接的概念有关。 我实现的解决scheme是添加“下一个”和“前一个”键到我发回的json映射并完成它。
后来我开始考虑可能要做的事情是发送一个307-临时redirect到一个类似于/ db / questions / 1,25的链接 – 这会使原来的URI成为规范的资源名称,但是它会让你一个适当命名的下属资源。 这是我想从413中看出的行为,但是307似乎是一个很好的折衷。 实际上还没有在代码中试过这个。 更好的办法是将redirectredirect到包含最近问题的实际ID的URL。 例如,如果每个问题都有一个整数ID,并且系统中有100个问题,而您想要显示最近的十个问题,那么对/ db / questions的请求应该是307'd到/ db / questions / 100,91
这是一个非常好的问题,谢谢你的提问。 你确定我是花了好几天思考这个事情的。
你可以检测Range
头,模仿Dojo,如果它不存在,模仿Atom。 在我看来,这整齐划分了用例。 如果您正在响应来自您的应用程序的REST查询,您希望它将被格式化为Range
标头。 如果您正在响应一个随便的浏览器,那么如果您返回分页链接,它将使工具提供一个简单的方法来探索收集。
范围标题的一个大问题是很多企业代理将它们过滤出来。 我build议使用查询参数。
随着rfc723x的发布, 未注册的范围单位确实违背了规范中的明确build议 。 考虑rfc7233 (弃用rfc2616):
“ 新的范围单位应该向IANA注册 ”(以及对HTTP范围单位注册的参考)。
在我看来,做到这一点的最好方法是将范围作为查询参数。 例如GET / db / questions /?date> mindate&date <maxdate 。 在GET / db / questions /没有查询参数的情况下,使用Location:/ db / questions /?query-parameters-to-retrieve-default-page返回303。 然后提供一个不同的URL,通过这个URL,谁在使用你的API来获得有关集合的统计信息(例如,如果他/她想要整个集合,使用哪个查询参数);
虽然有可能为此使用Range标题,但我不认为这是意图。 它似乎被devise用于处理片状连接以及限制数据(所以客户可以请求部分请求,如果有东西丢失或者尺寸太大而无法处理)。 您正在将分页切入可能用于通信层的其他目的。 处理分页的“正确”方式是返回的types。 而不是返回问题对象,而应该返回一个新types。
所以如果问题是这样的:
<questions> <question index=1></question> <question index=2></question> ... </questions>
新的types可能是这样的:
<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>
当然,你可以控制你的媒体types,所以你可以让你的“页面”适合你的需要。 如果你是通用的,你可以在客户端有一个parsing器来处理所有types的分页。 我认为这更符合HTTP规范的精神,而不是冒用Range参数来做其他事情。