我喜欢一些一些帮助处理一个奇怪的边缘情况与分页API我正在建设。

与许多api一样,这个api也会分页较大的结果。如果你查询/foos,你会得到100个结果(即foo #1-100),和一个链接到/foos?Page =2,返回foo #101-200。

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

这对于试图获取所有foo的API使用者来说是一个问题——他们不会收到foo #101。

处理这种情况的最佳实践是什么?我们希望使它尽可能的轻量级(即避免为API请求处理会话)。来自其他api的示例将非常感谢!


当前回答

选项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"
    }
}

这样,你就不会漏掉任何元素。这种方法对于许多用例来说应该足够好了。但是,请记住以下几点:

当一个页面的所有元素都具有相同的时间戳时,您可能会陷入无休止的循环。 当具有相同时间戳的元素重叠在两个页面时,可以将多个元素多次交付给客户端。

您可以通过增加页面大小和使用精确到毫秒的时间戳来减少这些缺点。

选项B:带有延续令牌的扩展键集分页

要处理上面提到的常规键集分页的缺点,可以向时间戳添加偏移量,并使用所谓的“延续令牌”或“游标”。偏移量是该元素相对于具有相同时间戳的第一个元素的位置。通常,令牌具有类似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"
    }
}

令牌“1512757072_2”指向页面的最后一个元素,并表示“客户端已经获得了时间戳为1512757072的第二个元素”。这样,服务器就知道该往哪里继续。

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

有关此方法的更多信息,请参阅博客文章“使用延续令牌的Web API分页”。这种方法的一个缺点是实现起来很棘手,因为有许多需要考虑的极端情况。这就是为什么像continuation-token这样的库可以很方便(如果您使用Java/ JVM语言)。免责声明:我是这篇文章的作者和图书馆的合著者。

其他回答

你有几个问题。

首先,你有你引用的例子。

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

如果您没有对原始数据集进行快照,那么这就是现实。

你可以让用户创建一个显式快照:

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

把全套装备都寄过来。

参考API分页设计,我们可以通过游标来设计分页API

他们有一个概念,叫做游标,它是指向一行的指针。你可以对数据库说"在那之后返回100行"对于数据库来说,这要容易得多,因为很有可能通过带索引的字段来标识行。这样你就不需要获取和跳过这些行了,你可以直接跳过它们。 一个例子:

  GET /api/products
  {"items": [...100 products],
   "cursor": "qWe"}

API返回一个(不透明的)字符串,你可以使用它来检索下一页:

GET /api/products?cursor=qWe
{"items": [...100 products],
 "cursor": "qWr"}

实现方面有许多选项。通常,您有一些排序标准,例如,产品id。在这种情况下,您将使用一些可逆算法(比如哈希)对产品id进行编码。在接收到带有游标的请求时,对其进行解码并生成类似WHERE id >:cursor LIMIT 100的查询。

优势:

通过游标可以提高数据库的查询性能 处理好时,新内容插入到db查询

劣势:

使用无状态API生成前一个页面链接是不可能的

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

如果你真的需要适应这种边缘情况,你需要“记住”你停止的地方。jandjorgensen的建议是正确的,但我将使用保证唯一的字段,如主键。您可能需要使用多个字段。

按照Facebook的流程,您可以(也应该)缓存已经请求的页面,如果它们请求已经请求过的页面,则只返回已删除的行。

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

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

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

你的服务器应该负责返回新记录(加载更多的记录或通过拉取刷新的新记录)以及从["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]中删除的记录的id。

例子:- 如果你请求加载更多,那么你的请求应该看起来像这样:-

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]
}

现在假设你正在请求旧记录(加载更多),假设“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"]
}

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

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request
}

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

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

例子:- 如果你请求加载更多,那么你的请求应该看起来像这样:-

{
        "isRefresh" : false,
        "firstId" : "id1",
        "lastId" : "id10",
        "last_request_time" : 1421748005
}

您的服务器负责返回last_request_time之后删除的记录的id,以及返回last_request_time之后在“id1”和“id10”之间更新的记录。

{
        "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"]
}

拉到刷新:-

加载更多

再补充一下Kamilk的回答:https://www.stackoverflow.com/a/13905589

这在很大程度上取决于你处理的数据集有多大。小型数据集确实可以有效地进行偏移分页,但大型实时数据集确实需要游标分页。 找到了一篇精彩的文章,关于Slack如何随着数据集的增加而进化其api的分页,解释了每个阶段的积极和消极因素:https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12