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


当前回答

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(元素的总数)。

其他回答

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

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

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

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.

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

当你查询/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中的分页方式(向下滚动到底部,以我上面给出的格式查看分页链接)。

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

参考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客户端在URL中包含之前返回的集合的最后一个元素的键,并在SQL查询中添加一个WHERE子句(或者其他等价的东西,如果您不使用SQL),以便它只返回那些键大于此值的元素呢?

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

方法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"]
}

拉到刷新:-

加载更多