我正在用Node.js和mongoose写一个web应用程序。如何对我从.find()调用得到的结果进行分页?我想要一个功能可比的“限制50,100”在SQL。
当前回答
我发现了一种非常有效的方法并亲自实施,我认为这种方法是最好的,原因如下:
它不使用跳过,这使得时间复杂度不能很好地扩展; 它使用id来查询文档。在MongoDB中,id默认情况下是索引的,这使得查询它们非常快; 它使用精益查询,这些被认为是非常具有执行力的,因为他们从Mongoose中删除了很多“魔法”,并返回一个来自MongoDB的“原始”文档; 它不依赖于任何可能包含漏洞或具有易受攻击依赖项的第三方包。
唯一需要注意的是,Mongoose的一些方法,比如.save()在精益查询中不能很好地工作,这些方法在这篇很棒的博客文章中列出了,我真的推荐这个系列,因为它考虑了很多方面,比如类型安全(防止严重错误)和PUT/ PATCH。
我将提供一些上下文,这是一个Pokémon存储库,分页工作如下:API从req接收unsafeId。Express的body对象,我们需要将其转换为字符串以防止NoSQL注入(它可以是一个带有邪恶过滤器的对象),这个unsafeId可以是一个空字符串或上一页最后一项的ID,它是这样的:
/**
* @description GET All with pagination, will return 200 in success
* and receives the last ID of the previous page or undefined for the first page
* Note: You should take care, read and consider about Off-By-One error
* @param {string|undefined|unknown} unsafeId - An entire page that comes after this ID will be returned
*/
async readPages(unsafeId) {
try {
const id = String(unsafeId || '');
let criteria;
if (id) {
criteria = {_id: {$gt: id}};
} // else criteria is undefined
// This query looks a bit redundant on `lean`, I just really wanted to make sure it is lean
const pokemon = await PokemonSchema.find(
criteria || {},
).setOptions({lean: true}).limit(15).lean();
// This would throw on an empty page
// if (pokemon.length < 1) {
// throw new PokemonNotFound();
// }
return pokemon;
} catch (error) {
// In this implementation, any error that is not defined by us
// will not return on the API to prevent information disclosure.
// our errors have this property, that indicate
// that no sensitive information is contained within this object
if (error.returnErrorResponse) {
throw error;
} // else
console.error(error.message);
throw new InternalServerError();
}
}
现在,为了消费它并避免前端的off - by - 1错误,你可以像下面这样做,考虑到pokemons是从API返回的Pokémons文档的数组:
// Page zero
const pokemons = await fetchWithPagination({'page': undefined});
// Page one
// You can also use a fixed number of pages instead of `pokemons.length`
// But `pokemon.length` is more reliable (and a bit slower)
// You will have trouble with the last page if you use it with a constant
// predefined number
const id = pokemons[pokemons.length - 1]._id;
if (!id) {
throw new Error('Last element from page zero has no ID');
} // else
const page2 = await fetchWithPagination({'page': id});
这里需要注意的是,Mongoose ID总是连续的,这意味着任何新的ID总是比旧的ID大,这是这个答案的基础。
这种方法已经针对Off-By-One错误进行了测试,例如,页面的最后一个元素可能会作为下一个页面的第一个元素返回(重复),或者位于上一页最后一个元素和当前页面第一个元素之间的元素可能会消失。
当您处理完所有页面并在最后一个元素(一个不存在的元素)之后请求一个页面时,响应将是一个200 (OK)的空数组,这太棒了!
其他回答
在用Rodolphe提供的信息仔细研究了Mongoose API后,我想出了这个解决方案:
MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });
最好的方法(IMO)是在有限的集合或文档中使用跳过和限制BUT。
要在有限的文档中进行查询,可以使用特定的索引,例如DATE类型字段上的索引。见下图
let page = ctx.request.body.page || 1
let size = ctx.request.body.size || 10
let DATE_FROM = ctx.request.body.date_from
let DATE_TO = ctx.request.body.date_to
var start = (parseInt(page) - 1) * parseInt(size)
let result = await Model.find({ created_at: { $lte: DATE_FROM, $gte: DATE_TO } })
.sort({ _id: -1 })
.select('<fields>')
.skip( start )
.limit( size )
.exec(callback)
这是一个示例函数,用于获得具有分页和限制选项的技能模型的结果
export function get_skills(req, res){
console.log('get_skills');
var page = req.body.page; // 1 or 2
var size = req.body.size; // 5 or 10 per page
var query = {};
if(page < 0 || page === 0)
{
result = {'status': 401,'message':'invalid page number,should start with 1'};
return res.json(result);
}
query.skip = size * (page - 1)
query.limit = size
Skills.count({},function(err1,tot_count){ //to get the total count of skills
if(err1)
{
res.json({
status: 401,
message:'something went wrong!',
err: err,
})
}
else
{
Skills.find({},{},query).sort({'name':1}).exec(function(err,skill_doc){
if(!err)
{
res.json({
status: 200,
message:'Skills list',
data: data,
tot_count: tot_count,
})
}
else
{
res.json({
status: 401,
message: 'something went wrong',
err: err
})
}
}) //Skills.find end
}
});//Skills.count end
}
您可以使用skip()和limit(),但效率非常低。更好的解决方案是对索引字段加上limit()进行排序。 我们在Wunderflats发布了一个小库:https://github.com/wunderflats/goosepage 它用了第一种方法。
下面的代码是为我工作良好。 你也可以在countDocs查询中添加查找过滤器和user same来获得准确的结果。
export const yourController = async (req, res) => {
const { body } = req;
var perPage = body.limit,
var page = Math.max(0, body.page);
yourModel
.find() // You Can Add Your Filters inside
.limit(perPage)
.skip(perPage * (page - 1))
.exec(function (err, dbRes) {
yourModel.count().exec(function (err, count) { // You Can Add Your Filters inside
res.send(
JSON.stringify({
Articles: dbRes,
page: page,
pages: count / perPage,
})
);
});
});
};
推荐文章
- elasticsearch vs . MongoDB用于过滤应用程序
- ReferenceError: description没有定义NodeJs
- MongoDB记录所有查询
- 将一个二进制的NodeJS Buffer转换为JavaScript的ArrayBuffer
- 使用LIMIT/OFFSET运行查询,还可以获得总行数
- AngularJS只适用于单页应用程序吗?
- 如何在vue-cli项目中更改端口号
- MongoDB:如何找到安装的MongoDB的确切版本
- 同步和异步编程(在node.js中)的区别是什么?
- 如何编辑通过npm安装的节点模块?
- 如何使用mongoimport导入CSV文件?
- “node_modules”文件夹应该包含在git存储库中吗
- 使用package.json在全局和本地安装依赖项
- 在mongodb中存储日期/时间的最佳方法
- this.libOptions.parse不是一个函数