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.
var childSchema = new Schema({ name: 'string' });
var parentSchema = new Schema({
// Array of subdocuments
children: [childSchema],
// Single nested subdocuments. Caveat: single nested subdocs only work
// in mongoose >= 4.2.0
child: childSchema
});
Aside from code reuse, one important reason to use subdocuments is to create a path where there would otherwise not be one to allow for validation over a group of fields (e.g. dateRange.fromDate <= dateRange.toDate).
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.
var Parent = mongoose.model('Parent', parentSchema);
var parent = new Parent({ 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.
parent.save(callback);
Subdocuments have save
and validate
middleware 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) {
return next(new Error('#sadpanda'));
}
next();
});
var parent = new Parent({ children: [{ name: 'invalid' }] });
parent.save(function (err) {
console.log(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 order
var childSchema = new mongoose.Schema({ name: 'string' });
childSchema.pre('validate', function(next) {
console.log('2');
next();
});
childSchema.pre('save', function(next) {
console.log('3');
next();
});
var parentSchema = new mongoose.Schema({
child: childSchema,
});
parentSchema.pre('validate', function(next) {
console.log('1');
next();
});
parentSchema.pre('save', function(next) {
console.log('4');
next();
});
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
.
var doc = parent.children.id(_id);
MongooseArray methods such as push, unshift, addToSet, and others cast arguments to their proper types transparently:
var Parent = mongoose.model('Parent');
var parent = new Parent;
// create a comment
parent.children.push({ name: 'Liesl' });
var subdoc = parent.children[0];
console.log(subdoc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
subdoc.isNew; // true
parent.save(function (err) {
if (err) return handleError(err)
console.log('Success!');
});
Subdocs may also be created without adding them to the array by using the create method of MongooseArrays.
var newdoc = parent.children.create({ name: 'Aaron' });
Each subdocument has it's own remove method. For an array subdocument, this is equivalent to calling .pull()
on the subdocument. For a single nested subdocument, remove()
is equivalent to setting the subdocument to null
.
// Equivalent to `parent.children.pull(_id)`
parent.children.id(_id).remove();
// Equivalent to `parent.child = null`
parent.child.remove();
parent.save(function (err) {
if (err) return handleError(err);
console.log('the subdocs were removed');
});
Sometimes, you need to get the parent of a subdoc. You can access the parent using the parent()
function.
const schema = new Schema({
docArr: [{ name: String }],
singleNested: new Schema({ name: String })
});
const Model = mongoose.model('Test', schema);
const doc = new Model({
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 = new Schema({
level1: new Schema({
level2: new Schema({
test: String
})
})
});
const Model = mongoose.model('Test', schema);
const doc = new Model({ level1: { level2: 'test' } });
doc.level1.level2.parent() === doc; // false
doc.level1.level2.parent() === doc.level1; // true
doc.level1.level2.ownerDocument() === doc; // true
If you create a schema with an array of objects, mongoose will automatically convert the object to a schema for you:
var parentSchema = new Schema({
children: [{ name: 'string' }]
});
// Equivalent
var parentSchema = new Schema({
children: [new Schema({ name: 'string' })]
});
Similarly, single subdocuments also have a shorthand whereby you can omit wrapping the schema with an instance of Schema. However, for historical reasons, this alternate declaration must be enabled via an option (either on the parent schema instantiation or on the mongoose instance).
var parentSchema = new Schema({
child: { type: { name: 'string' } }
}, { typePojoToMixed: false });
// Equivalent
var parentSchema = new Schema({
child: new Schema({ name: 'string' })
});
// Not equivalent! Careful - a Mixed path is created instead!
var parentSchema = new Schema({
child: { type: { name: 'string' } }
});
Now that we've covered Subdocuments, let's take a look at querying.
© 2010 LearnBoost
Licensed under the MIT License.
https://mongoosejs.com/docs/subdocs.html