API分页最佳实践

2ar*_*lls 275 rest pagination api-design

我希望通过我正在构建的分页API来处理一个奇怪的边缘情况.

像许多API一样,这个API分散了很多结果.如果您查询/ foos,您将获得100个结果(即foo#1-100),以及指向/ foos?page = 2的链接,该链接应返回foo#101-200.

不幸的是,如果在API使用者进行下一次查询之前从数据集中删除了foo#10,/ foos?page = 2将偏移100并返回foos#102-201.

对于试图吸引所有泡沫的API消费者而言,这是一个问题 - 他们不会收到foo#101.

处理这个问题的最佳做法是什么?我们希望尽可能轻量级(即避免处理API请求的会话).其他API的例子将不胜感激!

ram*_*jan 168

我不完全确定你的数据是如何处理的,所以这可能有效,也可能不行,但你是否考虑过使用时间戳字段进行分页?

当您查询/ 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"
    }

}
Run Code Online (Sandbox Code Playgroud)

只是一个注释,只使用一个时间戳依赖于结果中隐含的"限制".您可能希望添加显式限制或使用until属性.

可以使用列表中的最后一个数据项动态确定时间戳.这似乎或多或少是Facebook在其图谱API中分页的方式(向下滚动到底部以我上面给出的格式查看分页链接).

一个问题可能是如果你添加一个数据项,但根据你的描述听起来它们会被添加到最后(如果没有,请告诉我,我会看看我是否可以改进这一点).

  • 时间戳不保证是唯一的.也就是说,可以使用相同的时间戳创建多个资源.所以这种方法的缺点是下一页可能会重复当前页面中的最后一个(几个?)条目. (29认同)
  • 另一个类似的选择是使用RFC 5988(第5节)中指定的链接头字段:http://tools.ietf.org/html/rfc5988#page-6 (5认同)
  • @prmatta实际上,取决于数据库实现[时间戳保证是唯一的](http://stackoverflow.com/a/10616532/437226). (4认同)
  • @jandjorgensen我喜欢你的提议,但你不需要资源链接中的某种信息,所以我们知道我们是上一次还是下一次?就像:"上一个":"http://api.example.com/foo?before=TIMESTAMP""下一个":"http://api.example.com/foo?since=TIMESTAMP2"我们也会使用我们的序列ID而不是时间戳.你觉得有什么问题吗? (3认同)
  • @jandjorgensen从您的链接:"时间戳数据类型只是一个递增的数字,并不保留日期或时间....在SQL Server 2008及更高版本中,**时间戳类型已重命名为rowversion**,大概是更好地反映其目的和价值." 因此,没有证据表明时间戳(实际包含时间值的时间戳)是唯一的. (2认同)

Wil*_*ung 28

你有几个问题.

首先,你有一个你引用的例子.

如果插入行,您也会遇到类似的问题,但在这种情况下,用户会获得重复数据(可以说比丢失数据更容易管理,但仍然存在问题).

如果您没有快照原始数据集,那么这只是生活中的一个事实.

您可以让用户制作显式快照:

POST /createquery
filter.firstName=Bob&filter.lastName=Eubanks
Run Code Online (Sandbox Code Playgroud)

结果如下:

HTTP/1.1 301 Here's your query
Location: http://www.example.org/query/12345
Run Code Online (Sandbox Code Playgroud)

然后你可以整天翻页,因为它现在是静态的.这可以相当轻,因为您可以捕获实际的文档键而不是整行.

如果用例只是您的用户想要(并且需要)所有数据,那么您可以简单地将它们提供给他们:

GET /query/12345?all=true
Run Code Online (Sandbox Code Playgroud)

然后发送整个套件.


kam*_*ilk 25

如果你有分页,你也可以通过一些键对数据进行排序.为什么不让API客户端在URL中包含先前返回的集合的最后一个元素的键,并WHERE在SQL查询中添加一个子句(或者等效的东西,如果你不使用SQL),这样它只返回那些元素.关键是大于这个值?

  • 这不是一个糟糕的建议,但只是因为你按值排序并不意味着它是一个"关键",即唯一. (4认同)

Moh*_*shi 18

根据您的服务器端逻辑,可能有两种方法.

方法1:当服务器不够智能处理对象状态时.

您可以将所有缓存的记录唯一ID发送到服务器,例如["id1","id2","id3","id4","id5","id6","id7","id8","id9", "id10"]和一个布尔参数,用于了解您是在请求新记录(拉动刷新)还是旧记录(加载更多).

您的服务器应负责返回新记录(通过pull刷新来加载更多记录或新记录)以及来自["id1","id2","id3","id4","id5","id5"的已删除记录的ID ID6" , "ID7", "ID8", "ID9", "ID10"].

示例: - 如果您要求加载更多,那么您的请求应如下所示: -

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]
}
Run Code Online (Sandbox Code Playgroud)

现在假设您正在请求旧记录(加载更多)并假设"id2"记录由某人更新,并且"id5"和"id8"记录从服务器中删除,那么您的服务器响应应如下所示: -

{
        "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"]
}
Run Code Online (Sandbox Code Playgroud)

但是在这种情况下,如果你有很多本地缓存记录假设为500,那么你的请求字符串将会像这样太长: -

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request
}
Run Code Online (Sandbox Code Playgroud)

方法2:当服务器足够智能以根据日期处理对象状态时.

您可以发送第一条记录的ID以及最后一条记录和上一个请求的纪元时间.这样,即使您有大量缓存记录,您的请求也总是很小

示例: - 如果您要求加载更多,那么您的请求应如下所示: -

{
        "isRefresh" : false,
        "firstId" : "id1",
        "lastId" : "id10",
        "last_request_time" : 1421748005
}
Run Code Online (Sandbox Code Playgroud)

您的服务器负责返回在last_request_time之后删除的已删除记录的ID,以及在"id1"和"id10"之间的last_request_time之后返回更新的记录.

{
        "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"]
}
Run Code Online (Sandbox Code Playgroud)

拉动刷新: -

在此输入图像描述

装载更多

在此输入图像描述


Bre*_*ley 14

找到最佳实践可能很难,因为大多数使用API​​的系统都不适合这种情况,因为它是一个极端优势,或者它们通常不会删除记录(Facebook,Twitter).Facebook实际上说,由于分页后进行过滤,每个"页面"可能没有请求的结果数量. https://developers.facebook.com/blog/post/478/

如果你真的需要适应这种边缘情况,你需要"记住"你离开的地方.jandjorgensen建议只是关于点,但我会使用保证像主键一样独特的字段.您可能需要使用多个字段.

在Facebook的流程之后,您可以(并且应该)缓存已经请求的页面,如果他们请求已经请求的页面,则返回已删除的已删除行的页面.

  • 我不同意.仅保留唯一ID根本不会占用太多内存.您不必无限期地保留数据,仅用于"会话".使用memcache很容易,只需设置过期时间(即10分钟). (3认同)
  • 这不是一个可接受的解决方案.这是相当多的时间和内存消耗.所有已删除的数据以及请求的数据都需要保存在内存中,如果同一用户不再请求任何条目,则可能根本不使用这些数据. (2认同)

Arc*_*ano 9

分页通常是一种"用户"操作,为了防止计算机和人脑过载,你通常会给出一个子集.然而,不是认为我们没有得到整个清单,而是问问重要性可能更好吗?

如果需要准确的实时滚动视图,那么请求/响应的REST API就不适合用于此目的.为此,您应该考虑使用WebSockets或HTML5 Server-Sent Events来让您的前端知道何时处理更改.

现在,如果需要获取数据的快照,我只需提供一个API调用,该调用在一个请求中提供所有数据而不进行分页.请注意,如果您拥有大量数据集,则需要一些可以对输出进行流式传输但不会临时将其加载到内存中的内容.

对于我的情况,我隐式指定一些API调用以允许获取整个信息(主要是参考表数据).您还可以保护这些API,以免损害您的系统.


小智 7

选项A:带有时间戳的键集分页

为了避免您提到的偏移分页的缺点,可以使用基于键集的分页。通常,实体具有说明其创建或修改时间的时间戳。此时间戳可用于分页:只需将最后一个元素的时间戳作为下一个请求的查询参数即可。服务器又将时间戳记用作过滤条件(例如WHERE modificationDate >= receivedTimestampParameter

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757071}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "lastModificationDate": 1512757072,
        "nextPage": "https://domain.de/api/elements?modifiedSince=1512757072"
    }
}
Run Code Online (Sandbox Code Playgroud)

这样,您将不会错过任何元素。对于许多用例,这种方法应该足够好。但是,请记住以下几点:

  • 当单个页面的所有元素具有相同的时间戳时,您可能会陷入无限循环。
  • 当具有相同时间戳的元素重叠两个页面时,您可以多次将许多元素传递给客户端。

您可以通过增加页面大小和使用毫秒精度的时间戳来减少这些弊端。

选项B:具有连续令牌的扩展键集分页

要解决上述常规键集分页的缺点,可以在时间戳上添加偏移量,并使用所谓的“ Continuation Token”或“ Cursor”。偏移量是具有相同时间戳的元素相对于第一个元素的位置。通常,令牌的格式为Timestamp_Offset。它已在响应中传递给客户端,并且可以提交给服务器以检索下一页。

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757072}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "continuationToken": "1512757072_2",
        "nextPage": "https://domain.de/api/elements?continuationToken=1512757072_2"
    }
}
Run Code Online (Sandbox Code Playgroud)

令牌“ 1512757072_2”指向页面的最后一个元素,并指出“客户端已经获得带有时间戳1512757072的第二个元素”。这样,服务器知道从哪里继续。

请注意,您必须处理两个请求之间元素发生更改的情况。这通常是通过向令牌添加校验和来完成的。该校验和是根据具有此时间戳的所有元素的ID计算得出的。因此,我们最终得到了这样的令牌格式:Timestamp_Offset_Checksum

有关此方法的更多信息,请查看博客文章“ 带有Continuation Tokens的Web API分页 ”。这种方法的一个缺点是难以实现,因为必须考虑许多极端情况。这就是为什么像continuation-token这样的库可以派上用场的原因(如果您使用的是Java / JVM语言)。免责声明:我是该帖子的作者,也是该库的合著者。


Shu*_*ava 6

只是添加到 Kamilk 的这个答案:https ://www.stackoverflow.com/a/13905589

很大程度上取决于您正在处理的数据集有多大。小数据集在偏移分页上确实有效,但大型实时数据集确实需要游标分页。

找到了一篇关于Slack如何随着数据集的增加而改进其 api 分页的精彩文章,解释了每个阶段的正面和负面:https : //slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12


adn*_*leb 5

RESTFul API 中分页的另一个选项是使用此处介绍的 Link 标头。例如 Github使用如下:

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"
Run Code Online (Sandbox Code Playgroud)

可能的值为relfirst、last、next、previous。但通过使用Linkheader,可能无法指定total_count(元素总数)。