API分页最佳实践
我想用我正在构build的分页API来处理一个奇怪的边界情况。
像许多API一样,这个分页大的结果。 如果你查询/ foos,你会得到100个结果(即foo#1-100),并且到/ foos?page = 2的链接应该返回foo#101-200。
不幸的是,如果在API消费者进行下一个查询之前从数据集中删除foo#10,那么/ foos?page = 2将偏移100,并返回foos#102-201。
对于试图拉动所有foos的API消费者来说,这是一个问题 – 他们不会收到foo#101。
处理这个问题的最佳做法是什么? 我们希望尽可能轻量化(即避免处理API请求的会话)。 其他API的例子将不胜感激!
我不完全确定你的数据是如何处理的,所以这可能会也可能不会,但是你有没有考虑用时间戳字段进行分页?
当你查询/ foos你会得到100个结果。 然后你的API应该返回这样的东西(假设JSON,但如果它需要XML,可以遵循相同的原则):
{ "data" : [ { data item 1 with all relevant fields }, { data item 2 }, ... { data item 100 } ], "paging": { "previous": "http://api.example.com/foo?since=TIMESTAMP1" "next": "http://api.example.com/foo?since=TIMESTAMP2" } }
请注意,只有使用一个时间戳才会在结果中隐含“限制”。 你可能想添加一个明确的限制,或者也可以使用一个until
属性。
时间戳可以使用列表中的最后一个数据项来dynamic确定。 这似乎或多或less是如何Facebook分页在其graphicsAPI (向下滚动到底部看到我给上述格式的分页链接)。
一个问题可能是如果你添加一个数据项,但根据你的描述,它听起来像将被添加到最后(如果没有,让我知道,我会看看如果我可以改善这一点)。
你有几个问题。
首先,你有你引用的例子。
如果插入行,也会遇到类似的问题,但在这种情况下,用户将获得重复的数据(可以说比缺less数据更容易pipe理,但仍是一个问题)。
如果你不是快照原始数据集,那么这只是一个事实。
你可以让用户做一个明确的快照:
POST /createquery filter.firstName=Bob&filter.lastName=Eubanks
结果如下:
HTTP/1.1 301 Here's your query Location: http://www.example.org/query/12345
那么你可以整天翻页,因为它现在是静态的。 这可以是相当轻的重量,因为您可以捕获实际的文档键而不是整行。
如果用例仅仅是你的用户需要(并且需要)所有的数据,那么你可以直接给它们:
GET /query/12345?all=true
只是发送整个套件。
如果你有分页,你也可以通过一些键来sorting数据。 为什么不让API客户端在URL中包含以前返回的集合的最后一个元素的关键字,并向SQL查询中添加一个WHERE
子句(或者如果不使用SQL,则是等价的),以便只返回那些元素哪个关键大于这个值?
根据您的服务器端逻辑,可能有两种方法。
方法1:服务器不够聪明来处理对象状态。
例如[“id1”,“id2”,“id3”,“id4”,“id5”,“id6”,“id7”,“id8”,“id9”等)都可以发送到服务器。 “id10”]和一个布尔参数来知道你是否正在请求新的logging(拉刷新)或旧logging(加载更多)。
你的服务器应该负责返回新的logging(通过拉刷新加载更多logging或新logging)以及从[“id1”,“id2”,“id3”,“id4”,“id5”,“ ID6" , “ID7”, “ID8”, “ID9”, “ID10”]。
例如: –如果你要求加载更多,那么你的请求应该看起来像这样:
{ "isRefresh" : false, "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"] }
现在假设你正在请求旧logging(加载更多),并假设“id2”logging被某人更新,“id5”和“id8”logging从服务器中删除,那么你的服务器响应应该是这样的:
{ "records" : [ {"id" :"id2","more_key":"updated_value"}, {"id" :"id11","more_key":"more_value"}, {"id" :"id12","more_key":"more_value"}, {"id" :"id13","more_key":"more_value"}, {"id" :"id14","more_key":"more_value"}, {"id" :"id15","more_key":"more_value"}, {"id" :"id16","more_key":"more_value"}, {"id" :"id17","more_key":"more_value"}, {"id" :"id18","more_key":"more_value"}, {"id" :"id19","more_key":"more_value"}, {"id" :"id20","more_key":"more_value"}], "deleted" : ["id5","id8"] }
但在这种情况下,如果你有很多本地cachinglogging假设500,那么你的请求string将会像这样太长:
{ "isRefresh" : false, "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request }
方法2:当服务器足够聪明,以根据date处理对象状态。
您可以发送第一条logging的ID和最后一条logging以及之前的请求历元时间。 通过这种方式,即使您拥有大量的cachinglogging,您的请求仍然很less
例如: –如果你要求加载更多,那么你的请求应该看起来像这样:
{ "isRefresh" : false, "firstId" : "id1", "lastId" : "id10", "last_request_time" : 1421748005 }
您的服务器负责返回在last_request_time后删除的已删除logging的id,并返回“id1”和“id10”之间的last_request_time之后的已更新logging。
{ "records" : [ {"id" :"id2","more_key":"updated_value"}, {"id" :"id11","more_key":"more_value"}, {"id" :"id12","more_key":"more_value"}, {"id" :"id13","more_key":"more_value"}, {"id" :"id14","more_key":"more_value"}, {"id" :"id15","more_key":"more_value"}, {"id" :"id16","more_key":"more_value"}, {"id" :"id17","more_key":"more_value"}, {"id" :"id18","more_key":"more_value"}, {"id" :"id19","more_key":"more_value"}, {"id" :"id20","more_key":"more_value"}], "deleted" : ["id5","id8"] }
拉刷新: –
装载更多
可能很难find最佳实践,因为大多数带有API的系统不适应这种情况,因为它是一个极端的优势,或者它们通常不会删除logging(Facebook,Twitter)。 Facebook实际上表示,每个“页面”可能没有分页后完成的过滤请求的结果数量。 https://developers.facebook.com/blog/post/478/
如果你真的需要适应这个边缘情况,你需要“记住”你离开的地方。 jandjorgensenbuild议只是关注点,但我会使用保证是唯一的像主键。 您可能需要使用多个字段。
在Facebook的stream程之后,您可以(也应该)caching已经请求的页面,并且只要返回那些已经过滤的已删除行,如果他们请求了他们已经请求的页面。
分页通常是一种“用户”操作,为了防止计算机和人脑中的超负荷,通常给出一个子集。 但是,并不是认为我们没有得到整个清单,而是问问题更重要?
如果需要精确的实时滚动视图,本质上请求/响应的REST API不适合用于此目的。 为此,您应该考虑WebSockets或HTML5服务器发送的事件,以便在处理更改时让前端知道。
现在,如果需要获取数据的快照,我只需提供一个API调用,即可在一个请求中提供所有数据,而无需分页。 请注意,如果你有一个大的数据集,你将需要一些能够实现输出的stream式传输,而不会暂时将其加载到内存中。
对于我的情况,我隐式指定一些API调用,以获取整个信息(主要是参考表数据)。 您也可以保护这些API,以免损害您的系统。
我认为目前你的API实际上是应该如此回应。 页面中前100条logging按您所维护对象的整体顺序排列。 你的解释告诉你正在使用某种订单ID来定义你的对象的分页顺序。
现在,如果您希望第2页始终应该从101开始并以200结尾,那么您必须将页面上的条目数设为variables,因为它们将被删除。
你应该做下面的伪代码:
page_max = 100 def get_page_results(page_no) : start = (page_no - 1) * page_max + 1 end = page_no * page_max return fetch_results_by_id_between(start, end)
我已经想了很长时间,并最终结束了我将在下面描述的解决scheme。 这是一个非常复杂的步骤,但是如果你确实做了这一步,那么你最终会得到你真正关注的东西,这是未来请求的确定性结果。
你被删除的项目的例子只是冰山一angular。 如果按color=blue
进行过滤,但是有人在请求之间改变项目颜色,该怎么办? 以可靠的方式抓取所有项目是不可能的 ,除非…我们执行修订历史logging 。
我已经实现了,实际上比我想象的要难。 以下是我所做的:
- 我使用自动增量ID列创build了一个表更新
changelogs
- 我的实体有一个
id
字段,但这不是主键 - 实体有一个
changeId
字段,它既是主键也是changeId
日志的外键。 - 无论用户何时创build,更新或删除logging,系统都会在
changelogs
插入一条新logging,抓取该标识并将其分配给实体的新版本,然后将其插入数据库 - 我的查询select最大changeId(按ID分组)和自我join,以获得所有logging的最新版本。
- filter应用于最近的logging
- 状态字段跟踪是否删除项目
- 最大changeId返回给客户端,并作为查询参数添加到后续请求中
- 由于只创build了新的更改,因此每个
changeId
表示创build更改时的基础数据的唯一快照。 - 这意味着您可以
changeId
caching具有参数changeId
的请求的结果。 结果永远不会过期,因为他们永远不会改变。 - 这也打开了令人兴奋的function,如回滚/还原,同步客户端caching等任何function,从历史变化中受益。