8

My app has several users, each user has documents. Each documents needs to have a sequence number, that may look something like this: 2013-1, 2013-2 (year and sequence number), or perhaps just a simple number: 1, 2, 3...

Currently, I am assigning the sequence number from user's settings when the Mongoose docuemnt is created. Based on that sequence number and the number format from user's settings, I am generating the final document number.

What I realized is that when 2 documents are created at the same time, they will get exactly the same number, because I am incrementing the sequence number in settings just after I have saved a document. But I am assigning the sequence number when I am creating (not saving yet) the document so the sequence number will be exactly the same for both documents.

I obviously need a way to handle this sequence number auto-incrementing at the moment of saving...

How can I assure that this number is unique and automatically incremented/generated?

ragulka
  • 4,312
  • 7
  • 48
  • 73
  • The [docs](hhttp://www.wiredprairie.us/blog/index.php/archives/1524) have two good examples. You could use the counters example and add a secondary key for the user Id so it's unique per user. – WiredPrairie Jan 22 '13 at 12:13
  • Give this library a shot: [mongodb-autoincrement](https://www.npmjs.com/package/mongodb-autoincrement) – Jacob McKay Apr 22 '15 at 04:22

4 Answers4

12

@emre and @WiredPraire pointed me to the right direction, but I wanted to provide a full Mongoose-compatible answer to my question. I ended up with the following solution:

var Settings = new Schema({
  nextSeqNumber: { type: Number, default: 1 }
});

var Document = new Schema({
  _userId: { type: Schema.Types.ObjectId, ref: "User" },
  number: { type: String }
});

// Create a compound unique index over _userId and document number
Document.index({ "_userId": 1, "number": 1 }, { unique: true });

// I make sure this is the last pre-save middleware (just in case)
Document.pre('save', function(next) {
  var doc = this;
  // You have to know the settings_id, for me, I store it in memory: app.current.settings.id
  Settings.findByIdAndUpdate( settings_id, { $inc: { nextSeqNumber: 1 } }, function (err, settings) {
    if (err) next(err);
    doc.number = settings.nextSeqNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
    next();
  });
});

Please note that with this method there is no way to require the number path in the schema, and there is no point as well, because it is automatically added.

Community
  • 1
  • 1
ragulka
  • 4,312
  • 7
  • 48
  • 73
  • 2
    +1! To elaborate on why this works, you need a blocking operation (a write, for instance) so that the asynchronous `save()` calls follow in order. The "simpler" solution of using Find or Count the collection you wish to increment on fails, because both Find and Count are non-blocking (read commands), so you can't rely on them to behave serially with (potentially) multiple asynch `save()` calls. – Raven Dreamer Jan 02 '14 at 15:02
  • 3
    Thanks for posting a code sample, this looks like the solution I've been looking for too. In case this is useful for anyone, I also adapted it so that the ID is only generated when calling save for creations and not updates: if (doc.isNew) { Settings.findByIdAndUpdate... } else { next(); } – Matt Wilson Apr 25 '14 at 21:14
  • are you sure something is not missing? I get this error "Object # has no method 'findByIdAndUpdate'" I think I have to build a model first, haven't I? – Darko Romanov Oct 03 '14 at 09:49
3

You can achieve that through:

  1. create sequence generator, which is just another document that keeps a counter of the last number.
  2. Use a mongoose middleware to update the auto increment the desired field.

Here is a working and tested example with the todo app.

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/todoApp');

// Create a sequence
function sequenceGenerator(name){
  var SequenceSchema, Sequence;

  SequenceSchema = new mongoose.Schema({
    nextSeqNumber: { type: Number, default: 1 }
  });

  Sequence = mongoose.model(name + 'Seq', SequenceSchema);

  return {
    next: function(callback){
      Sequence.find(function(err, data){
        if(err){ throw(err); }

        if(data.length < 1){
          // create if doesn't exist create and return first
          Sequence.create({}, function(err, seq){
            if(err) { throw(err); }
            callback(seq.nextSeqNumber);
          });
        } else {
          // update sequence and return next
          Sequence.findByIdAndUpdate(data[0]._id, { $inc: { nextSeqNumber: 1 } }, function(err, seq){
            if(err) { throw(err); }
            callback(seq.nextSeqNumber);
          });
        }
      });
    }
  };
}

// sequence instance
var sequence = sequenceGenerator('todo');

var TodoSchema = new mongoose.Schema({
  name: String,
  completed: Boolean,
  priority: Number,
  note: { type: String, default: '' },
  updated_at: { type: Date, default: Date.now }
});

TodoSchema.pre('save', function(next){
  var doc = this;
  // get the next sequence
  sequence.next(function(nextSeq){
    doc.priority = nextSeq;
    next();
  });
});

var Todo = mongoose.model('Todo', TodoSchema);

You can test it out in the node console as follows

function cb(err, data){ console.log(err, data); }
Todo.create({name: 'hola'}, cb);
Todo.find(cb);

With every newly created object the you will see the priority increasing. Cheers!

Adrian
  • 9,102
  • 4
  • 40
  • 35
2

This code is taken from MongoDB manual and it actually describes making the _id field auto increment. However, it can be applied to any field. What you want is to check whether the inserted value exists in database just after you inserted your document. If it is allready inserted, re increment the value then try to insert again. This way you can detect dublicate values and re-increment them.

while (1) {

    var cursor = targetCollection.find( {}, { f: 1 } ).sort( { f: -1 } ).limit(1);

    var seq = cursor.hasNext() ? cursor.next().f + 1 : 1;

    doc.f = seq;

    targetCollection.insert(doc);

    var err = db.getLastErrorObj();

    if( err && err.code ) {
        if( err.code == 11000 /* dup key */ )
            continue;
        else
            print( "unexpected error inserting data: " + tojson( err ) );
    }

    break;
}

In this example f is the field in your document that you want to auto increment. To make this work you need to make your field UNIQUE which can be done with indexes.

db.myCollection.ensureIndex( { "f": 1 }, { unique: true } )
emre nevayeshirazi
  • 18,983
  • 12
  • 64
  • 81
  • 1
    Hmm, that might work, but any idea how I would implement thsi with Mongoose? Also, I need to make sure the document numbers are unique in the context of one user. There can be 2 users, both can have only one document, with the number 1. So teh unique index should consider number and user _id. – ragulka Jan 22 '13 at 11:23
2

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 DocumentSchema here */

autoIncrement.initialize(mongoose.connection);
DocumentSchema.plugin(autoIncrement.plugin, 'Document');
var Document = mongoose.model('Document', DocumentSchema);

You only need to initialize the autoIncrement once.

moorara
  • 3,897
  • 10
  • 47
  • 60
  • It would be great if you could look into https://stackoverflow.com/questions/45357813/issue-with-mongoose-auto-increment-plugin-in-nested-models – Sona Shetty Jul 29 '17 at 15:07