正文
说起分页,最容易想起的就是offset+limit,在mongodb里面可以用skip+limit实现,应该说这是最容易实现的功能,与前端的交互来说非常合理,你只需告诉前端有多少页,以及每页的大小即可。
But~~~~
这样效率太低,因为无论是哪个数据库,都会强制扫描offset或者skip的数据,即不会用到索引,所以,越到后面,skip的数据越多,效率越低。(当然了,数据量不大的话效果不明显。)
解决方案是用条件直接筛选索引,通过索引的限定直接去取数据,这样效率最高。
举个栗子:
// 已创建createdAt索引
Order
.find({createdAt: {$gte: nextId}})
.sort({createdAt: -1})
.limit(limit)
.exec()
你需要分页展示订单信息,只要前端获取nextId即可,尤其是APP,目前大部分的APP展示大量信息的时候,会选择瀑布流加无限翻滚,就是到用户翻滚到页面尾部的时候才会去请求以及加载下一页,因此不需要给前端一共多少页这样的信息,只需要告诉还有没有数据即可。
具体方案
前端第一次请求
example.com/orders
API返回order数组,以及在返回的头部信息中有x-next-id: 123456
当用户快拉到底部时,请求第二页数据
example.com/orders?next…
API返回order数组,以及在返回的头部信息中有x-next-id: 12345678
...
最后一次API返回的数组中只有2个文档,这时候头部中无x-nexd-id信息,即无更多数据
具体实现:
// 已创建createdAt索引
const orders = await Order
.find({createdAt: {$gte: nextId}})
.sort({createdAt: -1})
.limit(limit + 1) //需要多取一个doc来获得nextId
.exec();
if (docs.length === limit + 1) {
return {
docs: docs.slice(0, -1),
nextId: docs[docs.length - 1]._id,
};
}
return {
docs,
nextId: null,
};
需要注意的地方
不知道你发现没有,上面这个其实是可能有问题的,因为当有重复的createdAt字段存在时,就会有问题:
比如limit10个,恰好createdAt字段全部相同,于是返回的nextId跟前一次的nextId相同,那么就会造成死循环了。当然,这是最差情况,只是出现重复数据的概率挺高。
解决方案很简单,直接用unique字段,或者加上unique字段,比如id字段,query的时候用两个字段一起筛选,返回的nextId也是包含着两个字段。
MongoID不能保证连续增长,因此,在这种情况下,不适合此种方案,如果是为了保证速度以及效率的话,可以放缓存,比如redis以及memcached之类的。