Subdocuments

Subdocuments are documents embedded in other documents. In Mongoose, this means you can nest schemas in other schemas. Mongoose has two distinct notions of subdocuments: arrays of subdocuments and single nested subdocuments.

const childSchema = newSchema({ name: 'string' }); const parentSchema = newSchema({ // Array of subdocumentschildren: [childSchema], // Single nested subdocumentschild: childSchema });

Note that populated documents are not subdocuments in Mongoose. Subdocument data is embedded in the top-level document. Referenced documents are separate top-level documents.

const childSchema = newSchema({ name: 'string' }); constChild = mongoose.model('Child', childSchema); const parentSchema = newSchema({ child: { type: mongoose.ObjectId, ref: 'Child' } }); constParent = mongoose.model('Parent', parentSchema); const doc = awaitParent.findOne().populate('child'); // NOT a subdocument. `doc.child` is a separate top-level document. doc.child;

What is a Subdocument?

Subdocuments are similar to normal documents. Nested schemas can have middleware, custom validation logic, virtuals, and any other feature top-level schemas can use. The major difference is that subdocuments are not saved individually, they are saved whenever their top-level parent document is saved.

constParent = mongoose.model('Parent', parentSchema); const parent = newParent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] }); parent.children[0].name = 'Matthew'; // `parent.children[0].save()` is a no-op, it triggers middleware but// does **not** actually save the subdocument. You need to save the parent// doc.await parent.save();

Subdocuments have save and validatemiddleware just like top-level documents. Calling save() on the parent document triggers the save() middleware for all its subdocuments, and the same for validate() middleware.

childSchema.pre('save', function(next) { if ('invalid' == this.name) { returnnext(newError('#sadpanda')); } next(); }); const parent = newParent({ children: [{ name: 'invalid' }] }); try { await parent.save(); } catch (err) { err.message; // '#sadpanda' }

Subdocuments' pre('save') and pre('validate') middleware execute before the top-level document's pre('save') but after the top-level document's pre('validate') middleware. This is because validating before save() is actually a piece of built-in middleware.

// Below code will print out 1-4 in orderconst childSchema = new mongoose.Schema({ name: 'string' }); childSchema.pre('validate', function(next) { console.log('2'); next(); }); childSchema.pre('save', function(next) { console.log('3'); next(); }); const parentSchema = new mongoose.Schema({ child: childSchema }); parentSchema.pre('validate', function(next) { console.log('1'); next(); }); parentSchema.pre('save', function(next) { console.log('4'); next(); });

Subdocuments versus Nested Paths

In Mongoose, nested paths are subtly different from subdocuments. For example, below are two schemas: one with child as a subdocument, and one with child as a nested path.

// Subdocumentconst subdocumentSchema = new mongoose.Schema({ child: new mongoose.Schema({ name: String, age: Number }) }); constSubdoc = mongoose.model('Subdoc', subdocumentSchema); // Nested pathconst nestedSchema = new mongoose.Schema({ child: { name: String, age: Number } }); constNested = mongoose.model('Nested', nestedSchema);

These two schemas look similar, and the documents in MongoDB will have the same structure with both schemas. But there are a few Mongoose-specific differences:

First, instances of Nested never have child === undefined. You can always set subproperties of child, even if you don't set the child property. But instances of Subdoc can have child === undefined.

const doc1 = newSubdoc({}); doc1.child === undefined; // true doc1.child.name = 'test'; // Throws TypeError: cannot read property...const doc2 = newNested({}); doc2.child === undefined; // falseconsole.log(doc2.child); // Prints 'MongooseDocument { undefined }' doc2.child.name = 'test'; // Works

Subdocument Defaults

Subdocument paths are undefined by default, and Mongoose does not apply subdocument defaults unless you set the subdocument path to a non-nullish value.

const subdocumentSchema = new mongoose.Schema({ child: new mongoose.Schema({ name: String, age: { type: Number, default: 0 } }) }); constSubdoc = mongoose.model('Subdoc', subdocumentSchema); // Note that the `age` default has no effect, because `child`// is `undefined`.const doc = newSubdoc(); doc.child; // undefined

However, if you set doc.child to any object, Mongoose will apply the age default if necessary.

doc.child = {}; // Mongoose applies the `age` default: doc.child.age; // 0

Mongoose applies defaults recursively, which means there's a nice workaround if you want to make sure Mongoose applies subdocument defaults: make the subdocument path default to an empty object.

const childSchema = new mongoose.Schema({ name: String, age: { type: Number, default: 0 } }); const subdocumentSchema = new mongoose.Schema({ child: { type: childSchema, default: () => ({}) } }); constSubdoc = mongoose.model('Subdoc', subdocumentSchema); // Note that Mongoose sets `age` to its default value 0, because// `child` defaults to an empty object and Mongoose applies// defaults to that empty object.const doc = newSubdoc(); doc.child; // { age: 0 }

Finding a Subdocument

Each subdocument has an _id by default. Mongoose document arrays have a special id method for searching a document array to find a document with a given _id.

const doc = parent.children.id(_id);

Adding Subdocs to Arrays

MongooseArray methods such as push, unshift, addToSet, and others cast arguments to their proper types transparently:

constParent = mongoose.model('Parent'); const parent = newParent(); // create a comment parent.children.push({ name: 'Liesl' }); const subdoc = parent.children[0]; console.log(subdoc); // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' } subdoc.isNew; // trueawait parent.save(); console.log('Success!');

You can also create a subdocument without adding it to an array by using the create() method of Document Arrays.

const newdoc = parent.children.create({ name: 'Aaron' });

Removing Subdocs

Each subdocument has its own deleteOne method. For an array subdocument, this is equivalent to calling .pull() on the subdocument. For a single nested subdocument, deleteOne() is equivalent to setting the subdocument to null.

// Equivalent to `parent.children.pull(_id)` parent.children.id(_id).deleteOne(); // Equivalent to `parent.child = null` parent.child.deleteOne(); await parent.save(); console.log('the subdocs were removed');

Parents of Subdocs

Sometimes, you need to get the parent of a subdoc. You can access the parent using the parent() function.

const schema = newSchema({ docArr: [{ name: String }], singleNested: newSchema({ name: String }) }); constModel = mongoose.model('Test', schema); const doc = newModel({ docArr: [{ name: 'foo' }], singleNested: { name: 'bar' } }); doc.singleNested.parent() === doc; // true doc.docArr[0].parent() === doc; // true

If you have a deeply nested subdoc, you can access the top-level document using the ownerDocument() function.

const schema = newSchema({ level1: newSchema({ level2: newSchema({ test: String }) }) }); constModel = mongoose.model('Test', schema); const doc = newModel({ level1: { level2: 'test' } }); doc.level1.level2.parent() === doc; // false doc.level1.level2.parent() === doc.level1; // true doc.level1.level2.ownerDocument() === doc; // true

Alternate declaration syntax for arrays

If you create a schema with an array of objects, Mongoose will automatically convert the object to a schema for you:

const parentSchema = newSchema({ children: [{ name: 'string' }] }); // Equivalentconst parentSchema = newSchema({ children: [newSchema({ name: 'string' })] });

Next Up

Now that we've covered Subdocuments, let's take a look at querying.

close