如何在 Node.js 中使用 Mongoose 进行分页?

发布于 2024-10-30 21:42:31 字数 131 浏览 1 评论 0 原文

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

I am writing a webapp with Node.js and mongoose. How can I paginate the results I get from a .find() call? I would like a functionality comparable to "LIMIT 50,100" in SQL.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(30

执笔绘流年 2024-11-06 21:42:31

我对这个问题中所接受的答案感到非常失望。这不会扩展。如果您阅读了cursor.skip()上的细则:

cursor.skip() 方法通常很昂贵,因为它要求服务器在开始返回结果之前从集合或索引的开头遍历以获取偏移量或跳过位置。随着偏移量(例如上面的 pageNumber)增加,cursor.skip() 将变得更慢并且更加占用 CPU 资源。对于较大的集合,cursor.skip() 可能会受到 IO 限制。

为了以可扩展的方式实现分页,将 limit() 与至少一个过滤条件结合起来,createdOn 日期适合多种用途。

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

I'm am very disappointed by the accepted answers in this question. This will not scale. If you read the fine print on cursor.skip( ):

The cursor.skip() method is often expensive because it requires the server to walk from the beginning of the collection or index to get the offset or skip position before beginning to return result. As offset (e.g. pageNumber above) increases, cursor.skip() will become slower and more CPU intensive. With larger collections, cursor.skip() may become IO bound.

To achieve pagination in a scaleable way combine a limit( ) along with at least one filter criterion, a createdOn date suits many purposes.

MyModel.find( { createdOn: { $lte: request.createdOnBefore } } )
.limit( 10 )
.sort( '-createdOn' )
悲歌长辞 2024-11-06 21:42:31

在根据 Rodolphe 提供的信息仔细研究 Mongoose API 后,我找到了这个解决方案:

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

After taking a closer look at the Mongoose API with the information provided by Rodolphe, I figured out this solution:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });
执笔绘流年 2024-11-06 21:42:31

使用 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
            })
        })
    })

Pagination using mongoose, express and jade - Here's a link to my blog with more detail

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
            })
        })
    })
☆獨立☆ 2024-11-06 21:42:31

您可以像这样链接:

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

使用 exec 执行查询

query.exec(callback);

You can chain just like that:

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

Execute the query using exec

query.exec(callback);
无畏 2024-11-06 21:42:31

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

例如:
?page=0&limit=25 // 这将被添加到您的 URL 中:http:localhost:5000?page=0&limit=25

因为它将是一个 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开始

In this case, you can add the query page and/ or limit to your URL as a query string.

For example:
?page=0&limit=25 // this would be added onto your URL: http:localhost:5000?page=0&limit=25

Since it would be a String we need to convert it to a Number for our calculations. Let's do it using the parseInt method and let's also provide some default values.

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);
    });

BTW
Pagination starts with 0

烟沫凡尘 2024-11-06 21:42:31

您可以使用一个名为 Mongoose Paginate 的小包,它会更容易。

$ 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);
  }
}

You can use a little package called Mongoose Paginate that makes it easier.

$ npm install mongoose-paginate

After in your routes or controller, just add :

/**
 * 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);
  }
}
久夏青 2024-11-06 21:42:31

查询:

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);
        });
});

Query:

search = productName

Params:

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);
        });
});
信仰 2024-11-06 21:42:31

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

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
      });
  });
 });

This is a example you can try this,

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
      });
  });
 });
月光色 2024-11-06 21:42:31

尝试使用 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){
 });

Try using mongoose function for pagination. Limit is the number of records per page and number of the page.

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){
 });
似狗非友 2024-11-06 21:42:31

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

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,您将获得 ie 5 个最新文档
  2. 如果您提供了last_doc_id 然后你会得到接下来的 ie 5 个文档。

Simple and powerful pagination solution

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: the last document id that you get

no_of_docs_required: the number of docs that you want to fetch i.e. 5, 10, 50 etc.

  1. If you don't provide the last_doc_id to the method, you'll get i.e. 5 latest docs
  2. If you've provided the last_doc_id then you'll get the next i.e. 5 documents.
不气馁 2024-11-06 21:42:31

这就是我在代码上所做的

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
    });

这就是我所做的。

This is what I done it on code

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
    });

That is how I done it.

压抑⊿情绪 2024-11-06 21:42:31

有一些很好的答案给出了使用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 => {

  })

There are some good answers giving the solution that uses skip() & limit(), however, in some scenarios, we also need documents count to generate pagination. Here's what we do in our projects:

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 => {

  })
谁的新欢旧爱 2024-11-06 21:42:31

这是我附加到所有模型的版本。为了方便,它依赖于下划线,为了性能,它依赖于异步。 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;

Here is a version that I attach to all my models. It depends on underscore for convenience and async for performance. The opts allows for field selection and sorting using the mongoose syntax.

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;
}

Attach it to your model schema.

MySchema.statics.findPaginated = findPaginated;
笔芯 2024-11-06 21:42:31

上面的答案很好。

对于那些喜欢 async-await 而不是
承诺!!

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
        });
    }
};

Above answer's holds good.

Just an add-on for anyone who is into async-await rather than
promise !!

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
        });
    }
};
梦亿 2024-11-06 21:42:31

您也可以使用以下代码行,

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 中工作

you can use the following line of code as well

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()

this code will work in latest version of mongo

記柔刀 2024-11-06 21:42:31

实现此目的的可靠方法是使用查询字符串从前端传递值。假设我们想要获取 page #2 并将输出限制 25 个结果
查询字符串将如下所示: ?page=2&limit=25 // 这将添加到您的 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);

我建议将此逻辑实现到中间件中,以便您可以将其用于各种路由/控制器。

A solid approach to implement this would be to pass the values from the frontend using a query string. Let's say we want to get page #2 and also limit the output to 25 results.
The query string would look like this: ?page=2&limit=25 // this would be added onto your URL: http:localhost:5000?page=2&limit=25

Let's see the code:

// 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);

I would suggest implementing this logic into middleware so you can be able to use it for various routes/ controllers.

花桑 2024-11-06 21:42:31

您可以使用 mongoose-paginate-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

You can do using mongoose-paginate-v2. For more info click here

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
风情万种。 2024-11-06 21:42:31

我找到了一种非常有效的方法并自己实现了它,我认为这种方法是最好的,原因如下:

  • 它不使用skip,时间复杂度不能很好地扩展;
  • 它使用 ID 来查询文档。 MongoDB 中默认对 Id 建立索引,使得查询速度非常快;
  • 它使用精益查询,众所周知,这些查询非常高效,因为它们消除了 Mongoose 的许多“魔力”,并返回来自 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-One 错误在前端,您可以按照以下方式进行操作,考虑到 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 总是大于旧的,这是这个答案的基础。

这种方法已经过 Off-By-One 错误测试,例如,页面的最后一个元素可以作为下一页的第一个元素(重复)返回,或者是上一页的最后一个元素和上一页的最后一个元素之间的元素。当前页面的第一页可能会消失。

当您完成所有页面并请求最后一个元素(不存在的元素)之后的页面时,响应将是一个带有 200(OK)的空数组,这太棒了!

I have found a very efficient way and implemented it myself, I think this way is the best for the following reasons:

  • It does not use skip, which time complexity doesn't scale well;
  • It uses IDs to query the document. Ids are indexed by default in MongoDB, making them very fast to query;
  • It uses lean queries, these are known to be VERY performative, as they remove a lot of "magic" from Mongoose and returns a document that comes kind of "raw" from MongoDB;
  • It doesn't depend on any third party packages that might contain vulnerabilities or have vulnerable dependencies.

The only caveat to this is that some methods of Mongoose, such as .save() will not work well with lean queries, such methods are listed in this awesome blog post, I really recommend this series, because it considers a lot of aspects, such as type security (which prevents critical errors) and PUT/ PATCH.

I will provide some context, this is a Pokémon repository, the pagination works as the following: The API receives unsafeId from the req.body object of Express, we need to convert this to string in order to prevent NoSQL injections (it could be an object with evil filters), this unsafeId can be an empty string or the ID of the last item of the previous page, it goes like this:

 /**
   * @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();
    }
  }

Now, to consume this and avoid Off-By-One errors in the frontend, you do it like the following, considering that pokemons is the Array of Pokémons documents that are returned from the API:

// 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});

As a note here, Mongoose IDs are always sequential, this means that any newer ID will always be greater than the older one, that is the foundation of this answer.

This approach has been tested agaisnt Off-By-One errors, for instance, the last element of a page could be returned as the first element of the following one (duplicated), or an element that is between the last of the previous page and the first of the current page might disappear.

When you are done with all the pages and request a page after the last element (one that does not exist), the response will be an empty array with 200 (OK), which is awesome!

本王不退位尔等都是臣 2024-11-06 21:42:31

有很多方法可以实现它,但我会使用两个

  1. find()
  2. aggregate()
const pageSize = 10;
const pageNumber = 1;

MyModel.find({})
  .sort({ createdAt: -1 })
  .skip(pageSize * (pageNumber - 1))
  .limit(pageSize)
  .exec((err, items) => {
    if (err) {
      // handle error
    }
    MyModel.countDocuments().exec((countError, count) => {
      if (countError) {
        // handle error
      }
      const totalPages = Math.ceil(count / pageSize);
      res.json({
        items,
        totalPages,
        currentPage: pageNumber,
      });
    });
  });

但在 find() 函数中,我我两次访问数据库,但是使用aggregate(),我们可以在一个查询中完成此操作

const pageSize = 10;
const pageNumber = 1;

MyModel.aggregate([
  { $sort: { createdAt: -1 } },
  { $skip: pageSize * (pageNumber - 1) },
  { $limit: pageSize },
  { $group: { _id: null, count: { $sum: 1 }, items: { $push: "$ROOT" } } },
]).exec((err, results) => {
  if (err) {
    // handle error
  }
  const { count, items } = results[0];
  const totalPages = Math.ceil(count / pageSize);
  res.json({
    items,
    totalPages,
    currentPage: pageNumber,
  });
});

现在,这取决于您的要求,您更喜欢哪一个。在大多数用例中,我更喜欢 aggregate() 而不是 find(),因为它提供了更多操作数据的工具。

There are many ways to implement it, but I will go with two

  1. find()
  2. aggregate()
const pageSize = 10;
const pageNumber = 1;

MyModel.find({})
  .sort({ createdAt: -1 })
  .skip(pageSize * (pageNumber - 1))
  .limit(pageSize)
  .exec((err, items) => {
    if (err) {
      // handle error
    }
    MyModel.countDocuments().exec((countError, count) => {
      if (countError) {
        // handle error
      }
      const totalPages = Math.ceil(count / pageSize);
      res.json({
        items,
        totalPages,
        currentPage: pageNumber,
      });
    });
  });

But here in find() function, I am hitting the database two times, but using aggregate(), we can do this in one query

const pageSize = 10;
const pageNumber = 1;

MyModel.aggregate([
  { $sort: { createdAt: -1 } },
  { $skip: pageSize * (pageNumber - 1) },
  { $limit: pageSize },
  { $group: { _id: null, count: { $sum: 1 }, items: { $push: "$ROOT" } } },
]).exec((err, results) => {
  if (err) {
    // handle error
  }
  const { count, items } = results[0];
  const totalPages = Math.ceil(count / pageSize);
  res.json({
    items,
    totalPages,
    currentPage: pageNumber,
  });
});

Now, it is up to your requirement, which one you prefer. In most use cases, I prefer aggregate() instead of find() as it gives more tools to manipulate data.

最美的太阳 2024-11-06 21:42:31

最简单、更快捷的方法是使用 objectId 进行分页
例子;

初始加载条件

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

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

页面下一个条件

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

页面下一个条件

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

在 mongoose 中

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);
});

The easiest and more speedy way is, paginate with the objectId
Example;

Initial load condition

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

Take the first and last ObjectId from response data

Page next condition

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

Page next condition

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

In mongoose

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);
});
好多鱼好多余 2024-11-06 21:42:31

最好的方法(IMO)是在有限的集合或文档内使用跳过和限制。

为了在有限的文档内进行查询,我们可以使用特定的索引,例如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)

The best approach (IMO) is to use skip and limit BUT within a limited collections or documents.

To make the query within limited documents, we can use specific index like index on a DATE type field. See that below

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)
酒解孤独 2024-11-06 21:42:31

最简单的分页插件。

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

将插件添加到架构中,然后使用模型分页方法:

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

Most easiest plugin for pagination.

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

Add plugin to a schema and then use model paginate method:

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
冰之心 2024-11-06 21:42:31
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.
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.
失退 2024-11-06 21:42:31

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

 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

}

This is example function for getting the result of skills model with pagination and limit options

 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

}

夏天碎花小短裙 2024-11-06 21:42:31

使用 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)

Using 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)
难理解 2024-11-06 21:42:31

MongoDB 官方博客有一个关于分页的条目,其中解释了为什么“跳过”可能会很慢并提供替代方案:https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-1

The MongoDB official blog has an entry about pagination, where they go through why 'skip' may be slow and offer alternatives: https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-1

似梦非梦 2024-11-06 21:42:31

下面的代码对我来说工作得很好。
您还可以在 countDocs 查询中添加查找过滤器和用户相同的内容以获得准确的结果。

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,
          })
        );
      });
    });
};

Below Code Is Working Fine For Me.
You can add finding filters also and user same in countDocs query to get accurate results.

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,
          })
        );
      });
    });
};
醉酒的小男人 2024-11-06 21:42:31
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);
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);
南街女流氓 2024-11-06 21:42:31

您可以像这样编写查询。

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 Bootstrap 在前端创建分页并在后端使用 mongoose 跳过和限制方法的大量信息。

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

You can write query like this.

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 : page number coming from client as request parameters.
per_page : no of results shown per page

If you are using MEAN stack following blog post provides much of the information to create pagination in front end using angular-UI bootstrap and using mongoose skip and limit methods in the backend.

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

日暮斜阳 2024-11-06 21:42:31

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

You can either use skip() and limit(), but it's very inefficient. A better solution would be a sort on indexed field plus limit().
We at Wunderflats have published a small lib here: https://github.com/wunderflats/goosepage
It uses the first way.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文