我喜欢一些一些帮助处理一个奇怪的边缘情况与分页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的示例将非常感谢!


当前回答

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

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

其他回答

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

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

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

我认为目前你的api的反应应该是这样的。页面上的前100条记录按照您所维护的对象的总体顺序排列。您的解释告诉我们,您正在使用某种排序id来定义分页对象的顺序。

现在,如果您希望第2页始终从101开始,到200结束,那么您必须将该页上的条目数量作为变量,因为它们可能会被删除。

你应该做如下的伪代码:

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)

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

当你查询/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属性。

时间戳可以使用列表中的最后一个数据项动态确定。这似乎或多或少是Facebook在其Graph 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语言)。免责声明:我是这篇文章的作者和图书馆的合著者。

我对此进行了长时间的思考,最终得出了下面我将描述的解决方案。这在复杂性上是一个相当大的进步,但如果你确实迈出了这一步,你最终会得到你真正想要的,这是未来请求的确定性结果。

你所举的项目被删除的例子只是冰山一角。如果您正在通过颜色=蓝色进行过滤,但有人在请求之间更改了项目的颜色,该怎么办?以分页方式可靠地获取所有项目是不可能的…除非…我们实现修订历史。

我已经实现了它,实际上它比我想象的要简单。以下是我所做的:

I created a single table changelogs with an auto-increment ID column My entities have an id field, but this is not the primary key The entities have a changeId field which is both the primary key as well as a foreign key to changelogs. Whenever a user creates, updates or deletes a record, the system inserts a new record in changelogs, grabs the id and assigns it to a new version of the entity, which it then inserts in the DB My queries select the maximum changeId (grouped by id) and self-join that to get the most recent versions of all records. Filters are applied to the most recent records A state field keeps track of whether an item is deleted The max changeId is returned to the client and added as a query parameter in subsequent requests Because only new changes are created, every single changeId represents a unique snapshot of the underlying data at the moment the change was created. This means that you can cache the results of requests that have the parameter changeId in them forever. The results will never expire because they will never change. This also opens up exciting feature such as rollback / revert, synching client cache etc. Any features that benefit from change history.