3.2.5. 分页食谱

本食谱介绍如何对视图结果进行分页。分页是一种用户界面 (UI) 模式,允许显示大量行(结果集),而无需一次将所有行加载到 UI 中。一个固定大小的子集,即页面,与下一个和上一个链接或按钮一起显示,这些链接或按钮可以将视窗在结果集上移动到相邻页面。

我们假设您熟悉创建和查询文档和视图,以及多个视图查询选项。

3.2.5.1. 示例数据

为了获得一些可操作的数据,我们将创建一个乐队列表,每个乐队一个文档

{ "name":"Biffy Clyro" }

{ "name":"Foo Fighters" }

{ "name":"Tool" }

{ "name":"Nirvana" }

{ "name":"Helmet" }

{ "name":"Tenacious D" }

{ "name":"Future of the Left" }

{ "name":"A Perfect Circle" }

{ "name":"Silverchair" }

{ "name":"Queens of the Stone Age" }

{ "name":"Kerub" }

3.2.5.2. 一个视图

我们需要一个简单的映射函数,它可以提供乐队名称的字母顺序列表。这应该很容易,但我们添加了一些额外的智能功能,以过滤掉乐队名称前面的“The”和“A”,并将它们放到正确的位置

function(doc) {
    if(doc.name) {
        var name = doc.name.replace(/^(A|The) /, "");
        emit(name, null);
    }
}

视图结果是乐队名称的字母顺序列表。现在假设我们想一次显示五个乐队名称,并有一个指向构成一页的接下来的五个名称的链接,以及一个指向上一页的链接(如果我们不在第一页)。

我们已经了解了如何在之前的文档中使用startkeylimitskip参数。我们将在本文中再次使用它们。首先,让我们看一下完整的结果集

{"total_rows":11,"offset":0,"rows":[
    {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null},
    {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null},
    {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null},
    {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null},
    {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null},
    {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null},
    {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null},
    {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null},
    {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age","value":null},
    {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null},
    {"id":"fd590d4ad53771db47b0406054f02243","key":"Tool","value":null}
]}

3.2.5.3. 设置

分页的机制非常简单

  • 显示第一页

  • 如果有更多行要显示,则显示下一个链接

  • 绘制后续页面

  • 如果这不是第一页,则显示上一个链接

  • 如果有更多行要显示,则显示下一个链接

或者在一个伪 JavaScript 代码片段中

var result = new Result();
var page = result.getPage();

page.display();

if(result.hasPrev()) {
    page.display_link('prev');
}

if(result.hasNext()) {
    page.display_link('next');
}

3.2.5.4. 分页

要从视图结果中获取前五行,您可以使用?limit=5查询参数

curl -X GET http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5

结果

{"total_rows":11,"offset":0,"rows":[
    {"id":"a0746072bba60a62b01209f467ca4fe2","key":"Biffy Clyro","value":null},
    {"id":"b47d82284969f10cd1b6ea460ad62d00","key":"Foo Fighters","value":null},
    {"id":"45ccde324611f86ad4932555dea7fce0","key":"Tenacious D","value":null},
    {"id":"d7ab24bb3489a9010c7d1a2087a4a9e4","key":"Future of the Left","value":null},
    {"id":"ad2f85ef87f5a9a65db5b3a75a03cd82","key":"Helmet","value":null}
]}

通过将total_rows值与我们的limit值进行比较,我们可以确定是否还有更多页面要显示。我们还通过offset成员知道我们位于第一页。我们可以计算skip=的值以获取下一页的结果

var rows_per_page = 5;
var page = (offset / rows_per_page) + 1; // == 1
var skip = page * rows_per_page; // == 5 for the first page, 10 for the second ...

因此,我们使用以下命令查询 CouchDB:

curl -X GET 'http://127.0.0.1:5984/artists/_design/artists/_view/by-name?limit=5&skip=5'

请注意,我们必须使用'(单引号)来转义&字符,该字符对我们执行 curl 的 shell 来说是特殊的。

结果

{"total_rows":11,"offset":5,"rows":[
    {"id":"a2f31cfa68118a6ae9d35444fcb1a3cf","key":"Nirvana","value":null},
    {"id":"67373171d0f626b811bdc34e92e77901","key":"Kerub","value":null},
    {"id":"3e1b84630c384f6aef1a5c50a81e4a34","key":"Perfect Circle","value":null},
    {"id":"84a371a7b8414237fad1b6aaf68cd16a","key":"Queens of the Stone Age",
    "value":null},
    {"id":"dcdaf08242a4be7da1a36e25f4f0b022","key":"Silverchair","value":null}
]}

实现hasPrev()hasNext()方法非常简单

function hasPrev()
{
    return page > 1;
}

function hasNext()
{
    var last_page = Math.floor(total_rows / rows_per_page) +
        (total_rows % rows_per_page);
    return page != last_page;
}

3.2.5.5. 分页(备用方法)

上述方法在 CouchDB 1.2 之前对较大的跳过值执行效率低下。此外,即使使用更新版本的 CouchDB,某些用例也可能需要以下备用方法。其中一个案例是当需要防止重复结果时。仅使用 skip,新文档可能会在分页期间插入,这可能会改变后续页面的起始偏移量。

正确的解决方案并不难。与其将结果集切分成大小相等的页面,我们一次查看 10 行,并使用startkey跳到接下来的 10 行。我们甚至使用 skip,但仅使用值 1。

以下是它的工作原理

  • 从视图中请求rows_per_page + 1

  • 显示rows_per_page行,store + 1行作为next_startkeynext_startkey_docid

  • 作为页面信息,保留startkeynext_startkey

  • 使用next_*值创建下一个链接,并使用其他值创建上一个链接

查找下一页的技巧非常简单。与其为一页请求 10 行,您请求 11 行,但只显示 10 行,并将第 11 行中的值用作下一页的startkey。填充指向上一页的链接就像将当前startkey传递到下一页一样简单。如果没有上一个startkey,我们就在第一页。如果我们获得的rows_per_page行或更少,我们将停止显示指向下一页的链接。这被称为链接列表分页,因为我们从一页到另一页,或从一个列表项到另一个列表项,而不是直接跳转到预先计算的页面。不过,有一个注意事项。你能发现它吗?

CouchDB 视图键不必是唯一的;您可以读取多个索引条目。如果一个键的索引条目多于应该在一页上的行数怎么办?startkey跳转到第一行,如果您没有额外的参数可以使用,您就完蛋了。具有相同值的视图键在内部按docid排序,即创建该视图行的文档的 ID。您可以使用startkey_docidendkey_docid参数来获取这些行的子集。对于分页,我们仍然不需要endkey_docid,但startkey_docid非常方便。除了startkeylimit之外,您还使用startkey_docid进行分页,当且仅当您获取的额外行(用于查找下一页)与当前startkey具有相同的键时。

重要的是要注意,*_docid参数仅在*key参数之外有效,并且仅用于进一步缩小单个键的视图结果集。它们不能单独使用(唯一的例外是内置的_all_docs 视图,该视图已按文档 ID 排序)。

这种方法的优点是所有键操作都可以在视图背后的超快速 B 树索引上执行。查找页面不包括不必要地扫描数百或数千行。

3.2.5.6. 跳转到页面

链接列表式分页的一个缺点是,您无法从页码和每页行数预先计算特定页面的行。跳转到特定页面实际上行不通。如果有人提出这个问题,我们的本能反应是,“甚至谷歌都没有这样做!”,而且我们往往能摆脱它。谷歌在第一页上总是假装找到另外 10 页的结果。只有当您点击第二页(很少有人真正这样做)时,谷歌才会显示减少的页面集。如果您浏览结果,您将获得指向前 10 页和后 10 页的链接,但不会更多。预先计算 20 页所需的startkeystartkey_docid是一项可行的操作,也是一种实用的优化,可以了解可能包含数万行或更多行的结果集中的每一页的行。

如果您确实需要在整个文档范围内跳转到页面(我们已经看到一些应用程序需要这样做),您仍然可以将整数值索引作为视图索引,并采用混合方法来解决分页问题。