ObjectIds in Mongoose
By default, MongoDB creates an _id
property on every document that's of type
ObjectId. Many
other databases use a numeric id property by default, but in MongoDB and
Mongoose, ids are objects by default.
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
const doc = new Model({ name: 'test' });
doc._id instanceof mongoose.Types.ObjectId; // true
typeof doc._id; // 'object'
doc._id; // '5d6ede6a0ba62570afcedd3a'
Casting
MongoDB ObjectIds are typically represented using a 24 hexadecimal character
string, like '5d6ede6a0ba62570afcedd3a'
. Mongoose casts 24 char strings
to ObjectIds for you based on your schema paths.
const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);
const doc = new Model({ testId: '5d6ede6a0ba62570afcedd3a' });
// `testId` is an ObjectId, Mongoose casts 24 hex char strings to
// ObjectIds for you automatically based on your schema.
doc.testId instanceof mongoose.Types.ObjectId; // true
There are several other values that Mongoose can cast to ObjectIds. The key lesson is that an ObjectId is 12 arbitrary bytes. Any 12 byte buffer or 12 character string is a valid ObjectId.
const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);
// Any 12 character string is a valid ObjectId, because the only defining
// feature of ObjectIds is that they have 12 bytes.
let doc = new Model({ testId: '12char12char' });
doc.testId instanceof mongoose.Types.ObjectId; // true
doc.testId; // '313263686172313263686172'
// Similarly, Mongoose will automatically convert buffers of length 12
// to ObjectIds.
doc = new Model({ testId: Buffer.from('12char12char') });
doc.testId instanceof mongoose.Types.ObjectId; // true
doc.testId; // '313263686172313263686172'
Getting the Timestamp from an ObjectId
ObjectIds encode the local time at which they were created. That means you can
usually pull the time that a document was created from its _id
.
const schema = mongoose.Schema({ testId: mongoose.ObjectId });
const Model = mongoose.model('Test', schema);
const doc = new Model({ testId: '313263686172313263686172' });
doc.testId.getTimestamp(); // '1996-02-27T01:50:32.000Z'
doc.testId.getTimestamp() instanceof Date; // true
Why ObjectIds?
Suppose you're building your own database, and you want to set a numeric id
property on each new document. The id
property should be increasing, so the
first document you insert gets id = 0
, then id = 1
, and so on.
Incrementing a counter is an easy problem in a single process. But what if you have multiple processes, like a sharded cluster? Now each process needs to be able to increment the counter, so whenever you insert a document you also need to increment a distributed counter. That can lead to unreliable performance if there's significant network latency between two processes, or unpredictable results if one process is down.
ObjectIds are designed to work around this problem. ObjectId conflicts are highly unlikely, so MongoDB can assign ids that are probably unique in a distributed system with no inter-process communication.