59

According to this mongodb article it is possible to auto increment a field and I would like the use the counters collection way.

The problem with that example is that I don't have thousands of people typing the data in the database using the mongo console. Instead I am trying to use mongoose.

So my schema looks something like this:

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

I have created the counters collection in the same database and added a page with the _id of 'entityId'. From here I am not sure how to use mongoose to update that page and get the incrementing number.

There is no schema for counters and I would like it to stay that way because this is not really an entity used by the application. It should only be used in the schema(s) to auto increment fields.

HMR
  • 37,593
  • 24
  • 91
  • 160
  • 1
    Schema default values can't be async so this won't work. If you search for "auto increment" in the mongoose [plugins page](http://plugins.mongoosejs.com/) you'll find some options. – JohnnyHK Feb 06 '15 at 13:47
  • @JohnnyHK Thank you for your reply. The plugins work on update event and was something I'd rather avoid, searching for auto increment got me to the mongodb article in the first place and npm installable plugins for mongoose that seem to be event based. – HMR Feb 07 '15 at 15:51
  • Can you please tell me which approach you have used ?? and also when there are multiple concurrent requests then how your solution handle that ?? – Sudhanshu Gaur Jul 17 '17 at 11:58
  • The problem is [sufficiently complex](https://stackoverflow.com/questions/28357965/mongoose-auto-increment/54781939#54781939) that you should use a plugin. – Dan Dascalescu Apr 15 '20 at 00:12

17 Answers17

79

Here is an example how you can implement auto-increment field in 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();
    });
});
edtech
  • 1,734
  • 20
  • 20
  • 1
    Where can I put this entitySchema.pre('save', callback); if I have each of my models in seperate files in a models folder? – imrek Aug 01 '15 at 07:28
  • 2
    You can put it right after declaring the Schema, in the same file – edtech Aug 05 '15 at 22:19
  • This is not a fool proof solution when it comes to concurrent requests -- it's possible that requests made very closely to one another will have the same number in the "sequence" – realisation Apr 06 '16 at 18:50
  • 3
    @realisation `findByIdAndUpdate` according to http://mongoosejs.com/docs/api.html#model_Model.findByIdAndUpdate issues a mongodb `findAndModify` which _is_ atomic. – Akos K Apr 08 '16 at 08:35
  • 3
    Can you please tell me when there are multiple concurrent requests will your solution work or not ?? and if yes then how ?? – Sudhanshu Gaur Jul 17 '17 at 11:56
  • 1
    The `findByIdAndUpdate` will accept the `id` itself as the first parameter. Therefore, no need to provide a query object `{_id: ''}` – Ahmad Baktash Hayeri Aug 15 '17 at 06:56
  • Also note that in order to use the return value from the model, you need to pass in an `options` object with at least `upsert: true` set in case the model doesn't exist. – Ahmad Baktash Hayeri Aug 15 '17 at 07:41
  • 8
    I realize this is a bit old but, wouldn't this increment the counter when you do ANY updates to entitySchema (eg. update a status), making all references to the uniqe id useless? Maybe check isNew first. – Mankind1023 May 03 '18 at 21:45
  • According to for example app.post(...) - where I want to use autoincrement - where I must insert this code and how it invoke? – andrzej Jun 28 '18 at 13:48
  • In counter schema should I not set unique:true for _id? – Jayadratha Mondal Oct 30 '19 at 10:12
  • is this operation atomic? – MartianMartian Nov 01 '22 at 05:21
32

You can use mongoose-auto-increment package as follows:

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

You only need to initialize the autoIncrement once.

moorara
  • 3,897
  • 10
  • 47
  • 60
  • 18
    That package is no longer maintained. The best maintained and documented package for mongoose autoincrement as of April 2020 is [`mongoose-sequence`](https://github.com/ramiel/mongoose-sequence). – Dan Dascalescu Apr 14 '20 at 23:56
16

So combining multiple answers, this is what I ended up using:

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);
Akash Agarwal
  • 2,326
  • 1
  • 27
  • 57
15

The most voted answer doesn't work. This is the fix:

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

The options parameters gives you the result of the update and it creates a new document if it doesn't exist. You can check here the official doc.

And if you need a sorted index check this doc

Alexis Tyler
  • 1,394
  • 6
  • 30
  • 48
cluny85
  • 175
  • 1
  • 3
  • According to example: app.post(...) - where I want to use autoincrement - where I must insert this code and how it invoke? – andrzej Jun 28 '18 at 14:23
  • 2
    `findByIdAndUpdateAsync` is **not** a method in Mongoose's documentation. Even the link in the answer points to `findByIdAndUpdate`. – Akash Agarwal Dec 19 '18 at 02:54
11

Attention!

As hammerbot and dan-dascalescu pointed out this does not work if you remove documents.

If you insert 3 documents with id 1, 2 and 3 - you remove 2 and insert another a new one it'll get 3 as id which is already used!

In case you don't ever remove documents, here you go:

I know this has already a lot of answers, but I would share my solution which is IMO short and easy understandable:

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

Make sure that entitySchema._id has type:Number. Mongoose version: 5.0.1.

Simon
  • 2,686
  • 2
  • 31
  • 43
  • Don't think it would work in every context, but it solves my problem. – Hayden Braxton Apr 26 '18 at 04:37
  • 8
    IMO this breaks if some document gets deleted of the table at some point... But anyways, it also works for my usecase – Hammerbot May 15 '18 at 12:34
  • As @Hammerbot suggested, getting the document count for the next sequence value is *very* dangerous, for [the same reason the author of the `mongoose-sequence` package explained](https://github.com/ramiel/mongoose-sequence/issues/79#issuecomment-613299568). – Dan Dascalescu Apr 15 '20 at 00:00
  • counting docs every time is crazy inefficient – MartianMartian Nov 01 '22 at 05:22
8

This problem is sufficiently complicated and there are enough pitfalls that it's best to rely on a tested mongoose plugin.

Out of the plethora of "autoincrement" plugins at http://plugins.mongoosejs.io/, the best maintained and documented (and not a fork) is mongoose sequence.

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
Pallamolla Sai
  • 2,337
  • 1
  • 13
  • 14
7

I've combined all the (subjectively and objectively) good parts of the answers, and came up with this code:

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

One of the benefits of this approach is that all the counter related logic is separate. You can store it in a separate file and use it for multiple models importing the CounterModel.

If you are going to increment the _id field, you should add its definition in your schema:

const entitySchema = new mongoose.Schema({
    _id: {
        type: Number,
        alias: 'id',
        required: true,
    },
    <...>
});
Tigran
  • 2,800
  • 1
  • 19
  • 19
4

test.pre("save",function(next){
    if(this.isNew){
        this.constructor.find({}).then((result) => {
            console.log(result)
            this.id = result.length + 1;
            next();
          });
    }
})
Sukhveer Singh
  • 2,847
  • 2
  • 10
  • 12
  • you'll want to put the `next()` outside of the `if` block for other circumstances. then make it asynchronous with `async function(next)` and `await this.constructor.find(...` so that the next() doesn't run before the database query finishes – PMull34 Mar 29 '23 at 19:01
  • 1
    additionally, if any earlier documents are ever deleted from the collection (i.e., not the last one entered), this won't work anymore. e.g., if you have 3 documents with id's 1,2,3. and you delete 2, then the length of the collection is now 2, but there's a document with id 3 in there. so `this.id = result.length + 1` will assign 3 to the id, which would be a duplicate – PMull34 Mar 30 '23 at 14:51
3

I didn't wan to use any plugin (an extra dependencie, initializing the mongodb connection apart from the one I use in the server.js, etc...) so I did an extra module, I can use it at any schema and even, I'm considering when you remove a document from the DB.

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

And how to use it:

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;

Hope it helps you.

(If this Is wrong, please, tell me. I've been having no issues with this, but, not an expert)

Alberto Rubio
  • 404
  • 4
  • 13
  • Interesting solution. My only concern is that if someone deletes a record from the entity collection, in your case `Work`, the auto-increment values generated using your function may not fulfil the purpose of acting as a primary key (*given that it was the purpose*). – Akash Agarwal Dec 18 '18 at 23:14
  • In the pre save function, how do you use `model` before it's been defined? – douira Sep 08 '19 at 15:40
1

Here is a proposal.

Create a separate collection to holds the max value for a model collection

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

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

Now for each needed schema, add a pre-save hook.

For example, let the collection name is 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();
 }

As findOneAndUpdate is an atomic operation, no two updates will return same seq value. Thus each of your insertion will get an incremental seq regardless of number of concurrent insertions. Also this can be extended to more complex auto incremental logic and the auto increment sequence is not limited to Number type

This is not a tested code. Test before you use until I make a plugin for mongoose.

Update I found that this plugin implemented related approach.

Shafi
  • 1,850
  • 3
  • 22
  • 44
  • That plugin is no longer maintained. See [this other answer](https://stackoverflow.com/questions/28357965/mongoose-auto-increment/54781939#54781939) for the best plugin. – Dan Dascalescu Apr 15 '20 at 00:09
1

After reading the previous good answers, here is my concise solution in TS:

counter.ts

import {Schema, model} from 'mongoose';

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

const Counter = model('counter', counterSchema);

export async function getCounter(counterName: string): Promise<number> {
    const count = await Counter.findByIdAndUpdate(
        counterName,
        {$inc: {seq: 1}},
        {new: true, upsert: true}
    );
    return count.seq;
}

test.ts

import {model, Schema} from 'mongoose';
import {getCounter} from "./counter";

const testSchema = new Schema(
    {
        seq: Number,
        name: {
            type: String,
            required: true
        }
    });

testSchema.pre('save', async function (next) {
    if (!this.isNew) return;
    this.seq = await getCounter('test');
    next();
})
export const Test = model('test', testSchema);

index.ts

import {Test} from "./test";

const testObj = new Test({name: 'TEST'});
Tianren Li
  • 41
  • 2
0

The answers seem to increment the sequence even if the document already has an _id field (sort, whatever). This would be the case if you 'save' to update an existing document. No?

If I'm right, you'd want to call next() if this._id !== 0

The mongoose docs aren't super clear about this. If it is doing an update type query internally, then pre('save' may not be called.

CLARIFICATION

It appears the 'save' pre method is indeed called on updates.

I don't think you want to increment your sequence needlessly. It costs you a query and wastes the sequence number.

mschwartz
  • 63
  • 3
0

I had an issue using Mongoose Document when assigning value to Schema's field through put(). The count returns an Object itself and I have to access it's property.

I played at @Tigran's answer and here's my output:

// My goal is to auto increment the internalId field
export interface EntityDocument extends mongoose.Document {
    internalId: number
}

entitySchema.pre<EntityDocument>('save', async function() {
    if(!this.isNew) return;

    const count = await counter.findByIdAndUpdate(
        {_id: 'entityId'},
        {$inc: {seq: 1}},
        {new: true, upsert: true}
    );

    // Since count is returning an array
    // I used get() to access its child
    this.internalId = Number(count.get('seq'))
});

Version: mongoose@5.11.10

Mon Pinoliad
  • 67
  • 1
  • 2
  • 11
0

None of above answer works when you have unique fields in your schema because unique check at db level and increment happen before db level validation, so you may skip lots of numbers in auto increments like above solutions
only in post save can find if data already saved on db or return error

schmea.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
    next(new Error('email must be unique'));
  } else {
    next(error);
  }
});

https://stackoverflow.com/a/41479297/10038067

that is why none of above answers are not like atomic operations auto increment in sql like dbs

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 13 '22 at 14:24
0

without using any packages, you can just sort your primary id field by descending order and then increment it by one.

id field must be Number type

Model.find().sort({id: 'desc'}).limit(1).exec(function(err,model){
console.log(err,model);

//model is an array
const newId = ++model[0].id

console.log('new id: ',newId)

});
-1

I use together @cluny85 and @edtech. But I don't complete finish this issues.

counterModel.findByIdAndUpdate({_id: 'aid'}, {$inc: { seq: 1} }, function(error,counter){ But in function "pre('save...) then response of update counter finish after save document. So I don't update counter to document.

Please check again all answer.Thank you.

Sorry. I can't add comment. Because I am newbie.

Tom Nguyen
  • 67
  • 6
-2
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) {
    if (this.isNew) {
        var doc = this;
        counter.findByIdAndUpdate({ _id: 'entityId' }, { $inc: { seq: 1 } }, { new: true, upsert: true })
            .then(function(count) {
                doc.testvalue = count.seq;
                next();
            })
            .catch(function(error) {
                throw error;
            });
    } else {
        next();
    }
});
Tính Ngô Quang
  • 4,400
  • 1
  • 33
  • 33
  • Can you explain what this does and how this is any different from the [top answer](https://stackoverflow.com/questions/28357965/mongoose-auto-increment/30164636#30164636) given 3 years earlier? – Dan Dascalescu Apr 15 '20 at 00:11