如何在Node.js与猫鼬分页?

我正在用Node.js和mongoose写一个web应用程序。如何对.find()调用得到的结果进行分页?我想要一个功能可比"LIMIT 50,100"在SQL。

336149 次浏览

你可以像这样串起来:

var query = Model.find().sort('mykey', 1).skip(2).limit(5)

使用exec执行查询

query.exec(callback);

在用Rodolphe提供的信息仔细研究了Mongoose API后,我想出了这个解决方案:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });

你可以使用一个名为猫鼬随意翻阅的小包来简化它。

$ npm install mongoose-paginate

在你的路由或控制器后,只需添加:

/**
* querying for `all` {} items in `MyModel`
* paginating by second page, 10 items per page (10 results, page 2)
**/


MyModel.paginate({}, 2, 10, function(error, pageCount, paginatedResults) {
if (error) {
console.error(error);
} else {
console.log('Pages:', pageCount);
console.log(paginatedResults);
}
}

使用mongoose, express和jade进行分页- 这里有我博客的链接,有更多细节

var perPage = 10
, page = Math.max(0, req.params.page)


Event.find()
.select('name')
.limit(perPage)
.skip(perPage * page)
.sort({
name: 'asc'
})
.exec(function(err, events) {
Event.count().exec(function(err, count) {
res.render('events', {
events: events,
page: page,
pages: count / perPage
})
})
})

这是一个版本,我附加到我所有的模型。为了方便,它依赖于下划线,为了性能,它依赖于异步。opts允许使用mongoose语法进行字段选择和排序。

var _ = require('underscore');
var async = require('async');


function findPaginated(filter, opts, cb) {
var defaults = {skip : 0, limit : 10};
opts = _.extend({}, defaults, opts);


filter = _.extend({}, filter);


var cntQry = this.find(filter);
var qry = this.find(filter);


if (opts.sort) {
qry = qry.sort(opts.sort);
}
if (opts.fields) {
qry = qry.select(opts.fields);
}


qry = qry.limit(opts.limit).skip(opts.skip);


async.parallel(
[
function (cb) {
cntQry.count(cb);
},
function (cb) {
qry.exec(cb);
}
],
function (err, results) {
if (err) return cb(err);
var count = 0, ret = [];


_.each(results, function (r) {
if (typeof(r) == 'number') {
count = r;
} else if (typeof(r) != 'number') {
ret = r;
}
});


cb(null, {totalCount : count, results : ret});
}
);


return qry;
}

将它附加到您的模型模式。

MySchema.statics.findPaginated = findPaginated;

我对这个问题的公认答案感到非常失望。这是无法缩放的。如果你读了游标上的小字。跳过():

cursor.skip()方法通常开销很大,因为它需要服务器从集合或索引的开始处遍历以获得偏移或跳过位置,然后才开始返回结果。随着偏移量(例如上面的pageNumber)的增加,cursor.skip()将变得更慢,更消耗CPU。对于较大的集合,cursor.skip()可能成为IO绑定。

要以可伸缩的方式将limit()与至少一个筛选条件结合起来实现分页,createdOn日期适合多种用途。

MyModel.find( { createdOn: { $lte: request.createdOnBefore } } )
.limit( 10 )
.sort( '-createdOn' )

这是我在代码中做的

var paginate = 20;
var page = pageNumber;
MySchema.find({}).sort('mykey', 1).skip((pageNumber-1)*paginate).limit(paginate)
.exec(function(err, result) {
// Write some stuff here
});

我就是这么做的。

您可以像这样编写查询。

mySchema.find().skip((page-1)*per_page).limit(per_page).exec(function(err, articles) {
if (err) {
return res.status(400).send({
message: err
});
} else {
res.json(articles);
}
});

page:来自客户端作为请求参数的页码 Per_page:每页显示的结果数目

如果你正在使用MEAN堆栈,下面的博客文章提供了很多信息,在前端使用angular-UI引导和在后端使用猫鼬跳过和限制方法创建分页。

参见:https://techpituwa.wordpress.com/2015/06/06/mean-js-pagination-with-angular-ui-bootstrap/

你可以使用skip()和limit(),但是效率很低。更好的解决方案是对索引字段加上limit()进行排序。 我们在Wunderflats发布了一个小库:https://github.com/wunderflats/goosepage 它使用第一种方法

这是一个你可以尝试的例子,

var _pageNumber = 2,
_pageSize = 50;


Student.count({},function(err,count){
Student.find({}, null, {
sort: {
Name: 1
}
}).skip(_pageNumber > 0 ? ((_pageNumber - 1) * _pageSize) : 0).limit(_pageSize).exec(function(err, docs) {
if (err)
res.json(err);
else
res.json({
"TotalCount": count,
"_Array": docs
});
});
});
如果你正在使用mongoose作为restful api的源,请查看 'restify-mongoose'及其查询。它内置了这个功能。< / p >

集合上的任何查询都提供了在这里有用的标头

test-01:~$ curl -s -D - localhost:3330/data?sort=-created -o /dev/null
HTTP/1.1 200 OK
link: </data?sort=-created&p=0>; rel="first", </data?sort=-created&p=1>; rel="next", </data?sort=-created&p=134715>; rel="last"
.....
Response-Time: 37

所以基本上你得到了一个通用服务器,它对集合的查询具有相对线性的加载时间。这是非常棒的,如果你想要进行自己的实现,可以参考一下。

在这种情况下,可以将查询page和/或limit作为查询字符串添加到URL中。

例如< p >: < br > ?page=0&limit=25 // this would be added onto your URL: http:localhost:5000?page=0&limit=25 < / p >

由于它将是String,我们需要将它转换为Number进行计算。让我们使用parseInt方法来实现它,并提供一些默认值。

const pageOptions = {
page: parseInt(req.query.page, 10) || 0,
limit: parseInt(req.query.limit, 10) || 10
}


sexyModel.find()
.skip(pageOptions.page * pageOptions.limit)
.limit(pageOptions.limit)
.exec(function (err, doc) {
if(err) { res.status(500).json(err); return; };
res.status(200).json(doc);
});

< >强顺便说一句 分页以0

开始
最简单和更快速的方法是,用objectId进行分页 例子;< / p >

初始加载条件

condition = {limit:12, type:""};

从响应数据中获取第一个和最后一个ObjectId

下一页条件

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c662d", lastId:"57762a4c875adce3c38c6615"};

下一页条件

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c6645", lastId:"57762a4c875adce3c38c6675"};

在猫鼬

var condition = {};
var sort = { _id: 1 };
if (req.body.type == "next") {
condition._id = { $gt: req.body.lastId };
} else if (req.body.type == "prev") {
sort = { _id: -1 };
condition._id = { $lt: req.body.firstId };
}


var query = Model.find(condition, {}, { sort: sort }).limit(req.body.limit);


query.exec(function(err, properties) {
return res.json({ "result": result);
});

尝试使用mongoose函数进行分页。限制是每页的记录数量和页的数量。

var limit = parseInt(body.limit);
var skip = (parseInt(body.page)-1) * parseInt(limit);


db.Rankings.find({})
.sort('-id')
.limit(limit)
.skip(skip)
.exec(function(err,wins){
});
app.get("/:page",(req,res)=>{
post.find({}).then((data)=>{
let per_page = 5;
let num_page = Number(req.params.page);
let max_pages = Math.ceil(data.length/per_page);
if(num_page == 0 || num_page > max_pages){
res.render('404');
}else{
let starting = per_page*(num_page-1)
let ending = per_page+starting
res.render('posts', {posts:data.slice(starting,ending), pages: max_pages, current_page: num_page});
}
});
});

最好的方法(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)
**//localhost:3000/asanas/?pageNo=1&size=3**


//requiring asanas model
const asanas = require("../models/asanas");




const fetchAllAsanasDao = () => {
return new Promise((resolve, reject) => {


var pageNo = parseInt(req.query.pageNo);
var size = parseInt(req.query.size);
var query = {};
if (pageNo < 0 || pageNo === 0) {
response = {
"error": true,
"message": "invalid page number, should start with 1"
};
return res.json(response);
}
query.skip = size * (pageNo - 1);
query.limit = size;


asanas
.find(pageNo , size , query)
.then((asanasResult) => {
resolve(asanasResult);
})
.catch((error) => {
reject(error);
});


});
}

最简单的分页插件。

https://www.npmjs.com/package/mongoose-paginate-v2

添加插件到一个模式,然后使用模型paginate方法:

var mongoose         = require('mongoose');
var mongoosePaginate = require('mongoose-paginate-v2');


var mySchema = new mongoose.Schema({
/* your schema definition */
});


mySchema.plugin(mongoosePaginate);


var myModel = mongoose.model('SampleModel',  mySchema);


myModel.paginate().then({}) // Usage

这是一个示例函数,用于获得具有分页和限制选项的技能模型的结果

 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

以上回答是正确的。

只是一个插件,任何人谁是异步等待而不是 承诺! !< / p >

const findAllFoo = async (req, resp, next) => {
const pageSize = 10;
const currentPage = 1;


try {
const foos = await FooModel.find() // find all documents
.skip(pageSize * (currentPage - 1)) // we will not retrieve all records, but will skip first 'n' records
.limit(pageSize); // will limit/restrict the number of records to display


const numberOfFoos = await FooModel.countDocuments(); // count the number of records for that model


resp.setHeader('max-records', numberOfFoos);
resp.status(200).json(foos);


} catch (err) {
resp.status(500).json({
message: err
});
}
};

也可以用async/await实现结果。

下面的代码示例使用hapi v17和mongoose v5的异步处理程序

{
method: 'GET',
path: '/api/v1/paintings',
config: {
description: 'Get all the paintings',
tags: ['api', 'v1', 'all paintings']
},
handler: async (request, reply) => {
/*
* Grab the querystring parameters
* page and limit to handle our pagination
*/
var pageOptions = {
page: parseInt(request.query.page) - 1 || 0,
limit: parseInt(request.query.limit) || 10
}
/*
* Apply our sort and limit
*/
try {
return await Painting.find()
.sort({dateCreated: 1, dateModified: -1})
.skip(pageOptions.page * pageOptions.limit)
.limit(pageOptions.limit)
.exec();
} catch(err) {
return err;
}


}
}

简单而强大的分页解决方案

async getNextDocs(no_of_docs_required: number = 5, last_doc_id?: string) {
let docs


if (!last_doc_id) {
// get first 5 docs
docs = await MySchema.find().sort({ _id: -1 }).limit(no_of_docs_required)
}
else {
// get next 5 docs according to that last document id
docs = await MySchema.find({_id: {$lt: last_doc_id}})
.sort({ _id: -1 }).limit(no_of_docs_required)
}
return docs
}

last_doc_id:你得到的最后一个文档id

no_of_docs_required:你想要获取的文档的数量,例如5、10、50等。

  1. 如果你不给方法提供last_doc_id,你会得到5个最新的文档
  2. 如果你已经提供了last_doc_id,那么你将得到下一个,即5个文档。

您也可以使用下面的代码行

per_page = parseInt(req.query.per_page) || 10
page_no = parseInt(req.query.page_no) || 1
var pagination = {
limit: per_page ,
skip:per_page * (page_no - 1)
}
users = await User.find({<CONDITION>}).limit(pagination.limit).skip(pagination.skip).exec()

这段代码将在最新版本的mongo中工作

使用这个简单的插件。

https://github.com/WebGangster/mongoose-paginate-v2

安装

npm install mongoose-paginate-v2
Usage Add plugin to a schema and then use model paginate method:

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');


const mySchema = new mongoose.Schema({
/* your schema definition */
});


mySchema.plugin(mongoosePaginate);


const myModel = mongoose.model('SampleModel',  mySchema);


myModel.paginate().then({}) // Usage

一个可靠的实现方法是使用查询字符串从前端传递值。假设我们想要获得< em > < / em >页 #2< em > < / em >限制输出到25日结果.
查询字符串看起来像这样:?page=2&limit=25 // this would be added onto your URL: http:localhost:5000?page=2&limit=25

让我们看看代码:

// We would receive the values with req.query.<<valueName>>  => e.g. req.query.page
// Since it would be a String we need to convert it to a Number in order to do our
// necessary calculations. Let's do it using the parseInt() method and let's also provide some default values:


const page = parseInt(req.query.page, 10) || 1; // getting the 'page' value
const limit = parseInt(req.query.limit, 10) || 25; // getting the 'limit' value
const startIndex = (page - 1) * limit; // this is how we would calculate the start index aka the SKIP value
const endIndex = page * limit; // this is how we would calculate the end index


// We also need the 'total' and we can get it easily using the Mongoose built-in **countDocuments** method
const total = await <<modelName>>.countDocuments();


// skip() will return a certain number of results after a certain number of documents.
// limit() is used to specify the maximum number of results to be returned.


// Let's assume that both are set (if that's not the case, the default value will be used for)


query = query.skip(startIndex).limit(limit);


// Executing the query
const results = await query;


// Pagination result
// Let's now prepare an object for the frontend
const pagination = {};


// If the endIndex is smaller than the total number of documents, we have a next page
if (endIndex < total) {
pagination.next = {
page: page + 1,
limit
};
}


// If the startIndex is greater than 0, we have a previous page
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit
};
}


// Implementing some final touches and making a successful response (Express.js)


const advancedResults = {
success: true,
count: results.length,
pagination,
data: results
}
// That's it. All we have to do now is send the `results` to the frontend.
res.status(200).json(advancedResults);

我建议将这个逻辑实现到中间件中,这样你就可以将它用于各种路由/控制器。

使用ts-mongoose-pagination

    const trainers = await Trainer.paginate(
{ user: req.userId },
{
perPage: 3,
page: 1,
select: '-password, -createdAt -updatedAt -__v',
sort: { createdAt: -1 },
}
)


return res.status(200).json(trainers)
let page,limit,skip,lastPage, query;
page = req.params.page *1 || 1;  //This is the page,fetch from the server
limit = req.params.limit * 1 || 1; //  This is the limit ,it also fetch from the server
skip = (page - 1) * limit;   // Number of skip document
lastPage = page * limit;   //last index
counts = await userModel.countDocuments() //Number of document in the collection


query = query.skip(skip).limit(limit) //current page


const paginate = {}


//For previous page
if(skip > 0) {
paginate.prev = {
page: page - 1,
limit: limit
}
//For next page
if(lastPage < counts) {
paginate.next = {
page: page + 1,
limit: limit
}
results = await query //Here is the final results of the query.

查询:

search = productName

参数:

page = 1

// Pagination
router.get("/search/:page", (req, res, next) => {
const resultsPerPage = 5;
let page = req.params.page >= 1 ? req.params.page : 1;
const query = req.query.search;


page = page - 1


Product.find({ name: query })
.select("name")
.sort({ name: "asc" })
.limit(resultsPerPage)
.skip(resultsPerPage * page)
.then((results) => {
return res.status(200).send(results);
})
.catch((err) => {
return res.status(500).send(err);
});
});

你可以使用mongoose- pagate -v2。有关更多信息点击这里

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');


const mySchema = new mongoose.Schema({
// your schema code
});
mySchema.plugin(mongoosePaginate);
const myModel = mongoose.model('SampleModel',  mySchema);


myModel.paginate().then({}) // Usage

有一些很好的答案给出了使用skip() &但是,在某些情况下,我们还需要使用Limit()来生成分页。以下是我们在项目中所做的:

const PaginatePlugin = (schema, options) => {
options = options || {}
schema.query.paginate = async function(params) {
const pagination = {
limit: options.limit || 10,
page: 1,
count: 0
}
pagination.limit = parseInt(params.limit) || pagination.limit
const page = parseInt(params.page)
pagination.page = page > 0 ? page : pagination.page
const offset = (pagination.page - 1) * pagination.limit


const [data, count] = await Promise.all([
this.limit(pagination.limit).skip(offset),
this.model.countDocuments(this.getQuery())
]);
pagination.count = count;
return { data, pagination }
}
}


mySchema.plugin(PaginatePlugin, { limit: DEFAULT_LIMIT })


// using async/await
const { data, pagination } = await MyModel.find(...)
.populate(...)
.sort(...)
.paginate({ page: 1, limit: 10 })


// or using Promise
MyModel.find(...).paginate(req.query)
.then(({ data, pagination }) => {


})
.catch(err => {


})

MongoDB官方博客有一个关于分页的条目,在那里他们讨论了为什么“skip”可能很慢,并提供了替代方案:https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-1

我发现了一种非常有效的方法并亲自实施,我认为这种方法是最好的,原因如下:

  • 它不使用跳过,这使得时间复杂度不能很好地扩展;
  • 它使用id来查询文档。在MongoDB中,id默认情况下是索引的,这使得查询它们非常快;
  • 它使用精益查询,这些被认为是非常具有执行性的,因为它们删除了很多“魔法”。从Mongoose返回一个“raw”格式的文档;从MongoDB;
  • 它不依赖于任何可能包含漏洞或具有易受攻击依赖项的第三方包。

唯一需要注意的是,Mongoose的一些方法,比如.save()在精益查询中不能很好地工作,这些方法在很棒的博文中列出,我真的推荐这个系列,因为它考虑了很多方面,比如类型安全(防止严重错误)和PUT/ PATCH。

我将提供一些上下文,这是一个Pokémon存储库,分页工作如下:API从Express的req.body对象接收unsafeId,我们需要将其转换为字符串,以防止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)的空数组,这太棒了!

const ITEMS_PER_PAGE = 2;


exports.getProducts = (req, res, next) => {
// + will turn the string to a number
const page = +req.query.page || 1;
let totalItems;
//Product model
Product.find()
.countDocuments()
.then((numProducts) => {
totalItems = numProducts;
return Product.find()
//If query param is 3, since ITEMS_PER_PAGE = 2, we skip 2*2 items
// we show only 5th and 6th item
.skip((page - 1) * ITEMS_PER_PAGE)
.limit(ITEMS_PER_PAGE);
})
.then((products) => {
res.render("shop/products", {
// maybe sending the products object to templating engine
});
})
.catch((err) => {
const error = new Error(err);
error.httpStatusCode = 500;
// if you are set express error handler, use this
// when we call next() with an argument passed in, we let express know, we skip all other middlewares, we move to error handling middleware


return next(error);
});
};

下面的代码是为我工作良好。 你也可以在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,
})
);
});
});
};
const page = req.query.page * 1 || 1;
const limit = req.query.limit * 1 || 1000;
const skip = (page - 1) * limit;


query = query.skip(skip).limit(limit);