我喜欢一些一些帮助处理一个奇怪的边缘情况与分页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语言)。免责声明:我是这篇文章的作者和图书馆的合著者。

其他回答

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"

rel的可能值是:first, last, next, previous。但是通过使用Link头,可能无法指定total_count(元素的总数)。

选项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语言)。免责声明:我是这篇文章的作者和图书馆的合著者。

分页通常是一个“用户”操作,为了防止计算机和人脑的过载,通常会给出一个子集。然而,与其认为我们没有得到完整的列表,不如问问它重要吗?

如果需要一个精确的实时滚动视图,本质上是请求/响应的REST api并不适合这个目的。为此,你应该考虑WebSockets或HTML5 Server-Sent Events,让你的前端知道何时处理更改。

现在,如果需要获得数据的快照,我将只提供一个API调用,在一个请求中提供所有数据,而不进行分页。请注意,如果您有一个大型数据集,您将需要一些可以执行输出流而不临时将其加载到内存中的东西。

对于我的例子,我隐式地指定了一些API调用来允许获取全部信息(主要是引用表数据)。您还可以保护这些api,使其不会损害您的系统。

你有几个问题。

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

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

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

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

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生成前一个页面链接是不可能的