Understanding `unique` in Mongoose
The unique
option tells Mongoose that each document must have a unique value for a given path.
For example, below is how you can tell Mongoose that a user's email
must be unique.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: {
type: String,
unique: true // `email` must be unique
}
});
const User = mongoose.model('User', userSchema);
If you try to create two users with the same email
, you'll get a duplicate key error.
// Throws `MongoError: E11000 duplicate key error collection...`
await User.create([
{ email: 'test@google.com' },
{ email: 'test@google.com' }
]);
const doc = new User({ email: 'test@google.com' });
// Throws `MongoError: E11000 duplicate key error collection...`
await doc.save();
Updates can also throw a duplicate key error. For example, if you create a user with a unique email address and then update their email address to a non-unique value, you'll get the same error.
await User.create({ email: 'test2@google.com' });
// Throws `MongoError: E11000 duplicate key error collection...`
await User.updateOne({ email: 'test2@google.com' }, { email: 'test@google.com' });
Index, Not Validator
A common gotcha is that the unique
option tells Mongoose to define a unique index. That means Mongoose does not check uniqueness
when you use validate()
.
await User.create({ email: 'sergey@google.com' });
const doc = new User({ email: 'sergey@google.com' });
await doc.validate(); // Does not throw an error
The fact that unique
defines an index as opposed to a validator is also important when
writing automated tests. If you drop the database the User
model is connected to, you'll
also delete the unique
index, and you will be able to save duplicates.
await mongoose.connection.dropDatabase();
// Succeeds because the `unique` index is gone!
await User.create([
{ email: 'sergey@google.com' },
{ email: 'sergey@google.com' }
]);
In production you normally wouldn't drop the database, so this is rarely an issue in production.
When writing Mongoose tests, we normally recommend using deleteMany()
to clear out data in between tests, rather than dropDatabase()
. This ensures that you delete all documents, without clearing out database-level configuration, like indexes and collations. deleteMany()
is also much faster than dropDatabase()
.
However, if you choose to drop the database between tests, you can use the Model.syncIndexes()
function to rebuild all unique indexes.
await mongoose.connection.dropDatabase();
// Rebuild all indexes
await User.syncIndexes();
// Throws `MongoError: E11000 duplicate key error collection...`
await User.create([
{ email: 'sergey@google.com' },
{ email: 'sergey@google.com' }
]);
Handling null
Values
Since null
is a distinct value, you cannot save two users that have a null
email. Similarly,
you cannot save two users that don't have an email
property.
// Throws because both documents have undefined `email`
await User.create([
{},
{}
]);
// Throws because both documents have null `email`
await User.create([
{ email: null },
{ email: null }
]);
One workaround is to make the email
property required
, which disallows null
and undefined
:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true // `email` must be unique
}
});
If you need email
to be unique unless it is not defined, you can instead define a sparse unique index on email
as shown below.
const userSchema = new mongoose.Schema({
email: {
type: String,
// `email` must be unique, unless it isn't defined
index: { unique: true, sparse: true }
}
});
User-Friendly Duplicate Key Errors
To make MongoDB E11000 error messages user-friendly, you should use the mongoose-beautiful-unique-validation plugin.
const schema = new Schema({ name: String });
schema.plugin(require('mongoose-beautiful-unique-validation'));
const CharacterModel = mongoose.model('Character', schema);
const doc = await CharacterModel.create({ name: 'Jon Snow' });
try {
// Try to create a document with the same `_id`. This will always fail
// because MongoDB collections always have a unique index on `_id`.
await CharacterModel.create(Object.assign({}, doc.toObject()));
} catch (error) {
// Path `_id` (5cc60c5603a95a15cfb9204d) is not unique.
error.errors['_id'].message;
}