3.2.5. 分页配方

这个配方解释了如何在视图结果上分页。分页是一种允许显示大量行的用户界面(UI)模式 (the result set )而不是一次将所有行加载到UI中。固定大小的子集 page ,将与下一个和上一个链接或按钮一起显示,这些链接或按钮可以移动 viewport 在结果集上转到相邻页。

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

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. 分页(替代方法)

在couchdb1.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 跳到第一行,如果CouchDB没有一个额外的参数供您使用,那么您就完蛋了。所有具有相同值的视图键在内部按以下方式排序: docid ,即创建该视图行的文档的ID。你可以使用 startkey_docidendkey_docid 参数获取这些行的子集。对于分页,我们仍然不需要 endkey_docid ,但是 startkey_docid 非常方便。除了 startkeylimit ,您也可以使用 startkey_docid 当且仅当为查找下一页而获取的额外行具有与当前页相同的键时,才进行分页 startkey .

需要注意的是, *_docid 参数仅适用于除 *key 参数,并且仅用于进一步缩小单个键的视图结果集的范围。它们不能单独工作(一个例外是内置的 _all_docs view 其已按文档ID排序)。

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

3.2.5.6. 跳转到页面

链表式分页的一个缺点是,您不能根据页码和每页的行数预先计算特定页的行数。跳转到一个特定的页面并不真正有效。如果这种担忧被提起,我们的本能反应是,“连谷歌都没有这么做!“我们往往能逃脱惩罚。谷歌总是假装在第一页找到10多页的结果。只有当你点击第二个页面(很少有人会这样做),Google才会显示一组缩减的页面。如果你翻页浏览结果,你会得到前10页和下10页的链接,但不会更多。预先计算必要的 startkeystartkey_docid 对于20页是一种可行的操作,也是一种实用的优化,以了解结果集中可能有数万行或更多行的每一页的行数。

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