Mongoose 4.10.0 just landed and brings with it several powerful features and bug fixes. The most +1-ed feature in this supporting unique in array definitions via the mongoose-unique-array plugin. This feature is implemented as a separate plugin because mongoose-unique-array does much more than simply create a unique index, it also ties in to validators and versioning. In this article, I'll explain how to use mongoose-unique-array and the caveats you need to be aware of when using it.

Why a Separate Plugin?

In mongoose 4.10.0, you can create a model with an array of strings that are supposed to be unique. You don't need the mongoose-unique-array plugin to create a unique index on an array:

const mongoose = require ( 'mongoose' ); mongoose.connect( 'mongodb://localhost:27017/test' ); mongoose.set( 'debug' , true ); async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [{ type: String , unique: true }] }); const M = mongoose.model( 'Test' , mySchema); await new Promise ((resolve, reject) => { M.once( 'index' , err => err ? reject(err) : resolve()); }); await M.create([{ names: [ 'Test' ] }, { names: [ 'Test' ] }]); console .log( 'done' ); } run().catch(error => console .error(error.stack));

However, MongoDB unique indexes do not prevent saving a document with duplicate values in the names array. MongoDB unique indexes prevent multiple documents from having the value 'Test' in their names arrays, but a single document can have duplicate values in names .

const mongoose = require ( 'mongoose' ); mongoose.connect( 'mongodb://localhost:27017/test' ); mongoose.set( 'debug' , true ); async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [{ type: String , unique: true }] }); const M = mongoose.model( 'Test' , mySchema); await new Promise ((resolve, reject) => { M.once( 'index' , err => err ? reject(err) : resolve()); }); await M.create([{ names: [ 'Test' , 'Test' ] }]); console .log( 'done' ); } run().catch(error => console .error(error.stack));

The mongoose-unique-array plugin adds a custom validator to the names array that makes sure the array has no duplicate values.

const mongoose = require ( 'mongoose' ); const uniqueArrayPlugin = require ( 'mongoose-unique-array' ); mongoose.connect( 'mongodb://localhost:27017/test' ); mongoose.set( 'debug' , true ); async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [{ type: String , unique: true }] }); mySchema.plugin(uniqueArrayPlugin); const M = mongoose.model( 'Test' , mySchema); await new Promise ((resolve, reject) => { M.once( 'index' , err => err ? reject(err) : resolve()); }); await M.create([{ names: [ 'Test' , 'Test' ] }]); console .log( 'done' ); } run().catch(error => console .error(error.stack));

This also works for document arrays:

async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [{ username: { type: String , unique: true } }] }); mySchema.plugin(uniqueArrayPlugin); const M = mongoose.model( 'Test' , mySchema); await new Promise ((resolve, reject) => { M.once( 'index' , err => err ? reject(err) : resolve()); }); await M.create([{ names: [{ username: 'Test' }, { username: 'Test' }] }]); console .log( 'done' ); } run().catch(error => console .error(error.stack, error.errors));

Concurrency Caveats

In the above cases, the entire array is in memory, so checking the array for duplicates is easy: mongoose just loops through the array. However, mongoose converts .push() into a $push operation in MongoDB. In other words, if you use push() instead of overwriting the array each time, mongoose might not actually have the entire array in memory. In this case, the custom validator will not work.

async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [ String ] }); const M = mongoose.model( 'Test' , mySchema); await M.create([{ names: [] }]); const [doc1, doc2] = await Promise .all([M.findOne(), M.findOne()]); doc1.names.push( 'Test' ); await doc1.save(); console .log( 'Before push' , doc2.names); doc2.names.push( 'Test' ); await doc2.save(); console .log( await M.findById(doc1._id)); } run().catch(error => console .error(error.stack, error.errors));

To cope with this edge case, mongoose-unique-array modifies the query mongoose uses to save() the document. It also sets the saveErrorIfNotFound option on your schema so a save() that doesn't successfully modify a document throws an error.

async function run ( ) { await mongoose.connection.dropDatabase(); const mySchema = new mongoose.Schema({ names: [{ type: String , unique: true }] }); mySchema.plugin(uniqueArrayPlugin); const M = mongoose.model( 'Test' , mySchema); await new Promise ((resolve, reject) => { M.once( 'index' , err => err ? reject(err) : resolve()); }); await M.create([{ names: [] }]); const [doc1, doc2] = await Promise .all([M.findOne(), M.findOne()]); doc1.names.push( 'Test' ); await doc1.save(); console .log( 'Before push' , doc2.names); doc2.names.push( 'Test' ); await doc2.save(); console .log( await M.findById(doc1._id)); } run().catch(error => console .error(error));

With debug mode on, you can see the query that mongoose uses to update doc2 . Notice that this query only pushes if every element of names is not equal to 'Test'. Check out the docs on MongoDB's $nin operator for more information.

Mongoose: tests.update({ _id: ObjectId("591f5f93b6966a66c59355fc"), names: { '$nin': [ 'Test' ] } }, { '$pushAll': { names: [ 'Test' ] }, '$inc': { __v: 1 } })

The mongoose-unique-array plugin only affects save() . If you use update validation, mongoose-unique-array will also help you if you $set an array. However, mongoose-unique-array does not help you if you use update() and $push .

Moving On

Mongoose 4.10 has 8 powerful new features and several small bug fixes, including aliasing and the ability to modify function parameters in pre hooks. Look forward to more articles on these new features, and make sure you upgrade to take advantage of these new features.