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.

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).

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.

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();
});

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.

var 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:

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' });

Removing Subdocs

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');
});

Parents of Subdocs

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

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:

var parentSchema = new Schema({
  children: [{ name: 'string' }]
});
// Equivalent
var parentSchema = new Schema({
  children: [new Schema({ name: 'string' })]
});

Alternate declaration syntax for single subdocuments

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' } }
});

Next Up

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