Mongoose自增

59
根据这篇mongodb文章,自动递增字段是可能的,我想使用计数器集合的方法。
那个例子的问题在于,我没有成千上万的人使用mongo控制台在数据库中输入数据。相反,我正在尝试使用mongoose。
因此,我的模式看起来像这样:
var entitySchema = mongoose.Schema({
  testvalue:{type:String,default:function getNextSequence() {
        console.log('what is this:',mongoose);//this is mongoose
        var ret = db.counters.findAndModify({
                 query: { _id:'entityId' },
                 update: { $inc: { seq: 1 } },
                 new: true
               }
        );
        return ret.seq;
      }
    }
});

我已在同一数据库中创建了计数器集合,并添加了一个带有'entityId'的页面。从这里开始,我不确定如何使用mongoose更新该页面并获取递增的数字。
计数器没有模式,我希望它保持这种状态,因为这不是应用程序使用的实体。它只应该在模式中用于自动递增字段。

1
模式默认值无法是异步的,因此这样做不起作用。如果您在mongoose插件页面中搜索“自动递增”,您将找到一些选项。 - JohnnyHK
@JohnnyHK 感谢您的回复。插件在更新事件上起作用,这是我宁愿避免的事情。在搜索自动递增时,我首先找到了mongodb文章和可通过npm安装的mongoose插件,似乎是基于事件的。 - HMR
请问您使用了哪种方法?当有多个并发请求时,您的解决方案如何处理? - Sudhanshu Gaur
问题足够复杂,你应该使用插件。 参考链接 - Dan Dascalescu
17个回答

79

以下是一个示例,展示了如何在Mongoose中实现自增字段:

var CounterSchema = Schema({
    _id: {type: String, required: true},
    seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);

var entitySchema = mongoose.Schema({
    testvalue: {type: String}
});

entitySchema.pre('save', function(next) {
    var doc = this;
    counter.findByIdAndUpdate({_id: 'entityId'}, {$inc: { seq: 1} }, function(error, counter)   {
        if(error)
            return next(error);
        doc.testvalue = counter.seq;
        next();
    });
});

1
如果我将每个模型放在一个名为models的文件夹中,我应该把entitySchema.pre('save', callback);放在哪里? - imrek
2
你可以在声明Schema之后立即将其放置在同一文件中。 - edtech
3
根据http://mongoosejs.com/docs/api.html#model_Model.findByIdAndUpdate所述,`findByIdAndUpdate`会发出一个MongoDB的`findAndModify`查询,这是原子操作。 - Akos K
3
请问,如果有多个并发请求,您的解决方案是否能够正常工作?如果可以,请问是如何实现的? - Sudhanshu Gaur
9
我知道这有点过时,但是,如果您对entitySchema进行任何更新(例如更新状态),是否会使计数器增加,从而使所有对唯一ID的引用变得无用?也许应该首先检查isNew。 - Mankind1023
显示剩余6条评论

32

您可以按照以下方式使用mongoose-auto-increment包:

var mongoose      = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');

/* connect to your database here */

/* define your CounterSchema here */

autoIncrement.initialize(mongoose.connection);
CounterSchema.plugin(autoIncrement.plugin, 'Counter');
var Counter = mongoose.model('Counter', CounterSchema);

只需要初始化autoIncrement一次。


19
该软件包已不再维护。截至2020年4月,mongoose自增的最佳维护和文档化软件包是mongoose-sequence - Dan Dascalescu

17

结合多个答案,这是我最终使用的代码:

counterModel.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

const counterSchema = new Schema(
  {
  _id: {type: String, required: true},
  seq: { type: Number, default: 0 }
  }
);

counterSchema.index({ _id: 1, seq: 1 }, { unique: true })

const counterModel = mongoose.model('counter', counterSchema);

const autoIncrementModelID = function (modelName, doc, next) {
  counterModel.findByIdAndUpdate(        // ** Method call begins **
    modelName,                           // The ID to find for in counters model
    { $inc: { seq: 1 } },                // The update
    { new: true, upsert: true },         // The options
    function(error, counter) {           // The callback
      if(error) return next(error);

      doc.id = counter.seq;
      next();
    }
  );                                     // ** Method call ends **
}

module.exports = autoIncrementModelID;

myModel.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

const autoIncrementModelID = require('./counterModel');

const myModel = new Schema({
  id: { type: Number, unique: true, min: 1 },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date },
  someOtherField: { type: String }
});

myModel.pre('save', function (next) {
  if (!this.isNew) {
    next();
    return;
  }

  autoIncrementModelID('activities', this, next);
});

module.exports = mongoose.model('myModel', myModel);

15

最被投票的答案无效。这是修复方法:

var CounterSchema = new mongoose.Schema({
    _id: {type: String, required: true},
    seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);

var entitySchema = mongoose.Schema({
    sort: {type: String}
});

entitySchema.pre('save', function(next) {
    var doc = this;
    counter.findByIdAndUpdateAsync({_id: 'entityId'}, {$inc: { seq: 1} }, {new: true, upsert: true}).then(function(count) {
        console.log("...count: "+JSON.stringify(count));
        doc.sort = count.seq;
        next();
    })
    .catch(function(error) {
        console.error("counter error-> : "+error);
        throw error;
    });
});

options参数会返回更新的结果,如果文档不存在则创建新文档。你可以查看官方文档

如果你需要排序索引,请查看这份文档


根据示例:app.post(...) - 我想使用自增 - 我必须在哪里插入这段代码以及如何调用它? - andrzej
2
findByIdAndUpdateAsync不是Mongoose文档中的一个方法。即使答案中的链接也指向findByIdAndUpdate - Akash Agarwal

11

注意!

正如 hammerbotdan-dascalescu 所指出的,如果你移除文档,这将不起作用

如果您插入 id 为 123 的 3 个文档 - 您删除 2 并插入一个新的文档,它将获得已经被使用的 3 作为 id!

如果您从未删除文档,请查看以下内容:

我知道这里已经有很多答案了,但我想分享我的解决方案,它在我看来简短且易于理解:

// Use pre middleware
entitySchema.pre('save', function (next) {

    // Only increment when the document is new
    if (this.isNew) {
        entityModel.count().then(res => {
            this._id = res; // Increment count
            next();
        });
    } else {
        next();
    }
});

请确保entitySchema._id的类型为 Number
Mongoose版本:5.0.1


不认为它适用于每个场景,但它解决了我的问题。 - Hayden Braxton
8
如果某个文件在某个时刻被从表格中删除,这将导致它失效......但无论如何,它对我的使用场景也是有效的。 - Hammerbot
正如@Hammerbot所建议的那样,获取下一个序列值的文档计数非常危险,原因与mongoose-sequence软件包的作者解释的相同。 - Dan Dascalescu
每次计算文档数量都非常低效。 - MartianMartian

8

这是一个时间节省器。Mongoose序列比自增更好。谢谢伙计。 - Surya

7

我将所有答案中(主观和客观)好的部分结合起来,得出了以下代码:

const counterSchema = new mongoose.Schema({
    _id: {
        type: String,
        required: true,
    },
    seq: {
        type: Number,
        default: 0,
    },
});

// Add a static "increment" method to the Model
// It will recieve the collection name for which to increment and return the counter value
counterSchema.static('increment', async function(counterName) {
    const count = await this.findByIdAndUpdate(
        counterName,
        {$inc: {seq: 1}},
        // new: return the new value
        // upsert: create document if it doesn't exist
        {new: true, upsert: true}
    );
    return count.seq;
});

const CounterModel = mongoose.model('Counter', counterSchema);


entitySchema.pre('save', async function() {
    // Don't increment if this is NOT a newly created document
    if(!this.isNew) return;

    const testvalue = await CounterModel.increment('entity');
    this.testvalue = testvalue;
});

这种方法的优点之一是所有与计数器相关的逻辑都是分开的。您可以将它存储在一个单独的文件中,并在导入CounterModel时用于多个模型。
如果您要增加_id字段的值,则应在模式中添加其定义:
const entitySchema = new mongoose.Schema({
    _id: {
        type: Number,
        alias: 'id',
        required: true,
    },
    <...>
});

4

test.pre("save",function(next){
    if(this.isNew){
        this.constructor.find({}).then((result) => {
            console.log(result)
            this.id = result.length + 1;
            next();
          });
    }
})


您需要将next()放在if块之外,以处理其他情况。然后使用async function(next)await this.constructor.find(...将其异步化,以确保数据库查询结束前不会运行 next()。 - PMull34
1
此外,如果从集合中删除了任何早期文档(即不是最后一个输入的文档),则这将不再起作用。例如,如果您有3个ID为1、2、3的文档,并删除2,则集合的长度现在为2,但其中仍有一个ID为3的文档。因此,this.id = result.length + 1将分配3给ID,这将是一个重复。 - PMull34

4

我不想使用任何插件(额外的依赖项,初始化mongodb连接除了我在server.js中使用的那个之外...),所以我做了一个额外的模块,我可以在任何模式下使用它,甚至考虑当您从数据库中删除文档时。

module.exports = async function(model, data, next) {
    // Only applies to new documents, so updating with model.save() method won't update id
    // We search for the biggest id into the documents (will search in the model, not whole db
    // We limit the search to one result, in descendant order.
    if(data.isNew) {
        let total = await model.find().sort({id: -1}).limit(1);
        data.id = total.length === 0 ? 1 : Number(total[0].id) + 1;
        next();
    };
};

如何使用:

const autoincremental = require('../modules/auto-incremental');

Work.pre('save', function(next) {
    autoincremental(model, this, next);
    // Arguments:
    // model: The model const here below
    // this: The schema, the body of the document you wan to save
    // next: next fn to continue
});

const model = mongoose.model('Work', Work);
module.exports = model;

希望这能帮助到你。

(如果有误,请告诉我。我自己使用没有问题,但并不是专家)


有趣的解决方案。我唯一的担忧是,如果有人从实体集合中删除了记录,在您的情况下是 Work,则使用您的函数生成的自增值可能无法履行作为主键的目的(如果它是目的的话)。 - Akash Agarwal
1
在预保存函数中,如何在定义之前使用 model - douira
而且你绝对希望在if语句之外调用next()函数。 - undefined

1

这里有一个提议。

创建一个单独的集合来保存模型集合的最大值。

const autoIncrementSchema = new Schema({
    name: String,
    seq: { type: Number, default: 0 }
});

const AutoIncrement = mongoose.model('AutoIncrement', autoIncrementSchema);

现在,对于每个需要的模式,添加一个“pre-save hook”。 例如,假设集合名称为“Test”。
schema.pre('save', function preSave(next) {
    const doc = this;
    if (doc.isNew) {
         const nextSeq = AutoIncrement.findOneAndUpdate(
             { name: 'Test' }, 
             { $inc: { seq: 1 } }, 
             { new: true, upsert: true }
         );

         nextSeq
             .then(nextValue => doc[autoIncrementableField] = nextValue)
             .then(next);
    }
    else next();
 }

作为一个原子操作,findOneAndUpdate不会返回相同的seq值。因此,每次插入都会获得一个增量序列,无论并发插入的数量如何此外,这可以扩展到更复杂的自动增量逻辑,并且自动增量序列不限于数字类型

这不是经过测试的代码,请在使用之前进行测试,直到我为mongoose制作插件。

更新 我发现this插件实现了相关方法。


那个插件已经不再维护了。请查看这个答案以获取最佳插件。 - Dan Dascalescu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接