Subdocuments

Sponsor #native_company# — #native_desc#

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.

  1. const childSchema = new Schema({ name: 'string' });
  2. const parentSchema = new Schema({
  3. // Array of subdocuments
  4. children: [childSchema],
  5. // Single nested subdocuments. Caveat: single nested subdocs only work
  6. // in mongoose >= 4.2.0
  7. child: childSchema
  8. });

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.

  1. const Parent = mongoose.model('Parent', parentSchema);
  2. const parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] })
  3. parent.children[0].name = 'Matthew';
  4. // `parent.children[0].save()` is a no-op, it triggers middleware but
  5. // does **not** actually save the subdocument. You need to save the parent
  6. // doc.
  7. 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.

  1. childSchema.pre('save', function (next) {
  2. if ('invalid' == this.name) {
  3. return next(new Error('#sadpanda'));
  4. }
  5. next();
  6. });
  7. const parent = new Parent({ children: [{ name: 'invalid' }] });
  8. parent.save(function (err) {
  9. console.log(err.message) // #sadpanda
  10. });

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.

  1. // Below code will print out 1-4 in order
  2. const childSchema = new mongoose.Schema({ name: 'string' });
  3. childSchema.pre('validate', function(next) {
  4. console.log('2');
  5. next();
  6. });
  7. childSchema.pre('save', function(next) {
  8. console.log('3');
  9. next();
  10. });
  11. const parentSchema = new mongoose.Schema({
  12. child: childSchema
  13. });
  14. parentSchema.pre('validate', function(next) {
  15. console.log('1');
  16. next();
  17. });
  18. parentSchema.pre('save', function(next) {
  19. console.log('4');
  20. next();
  21. });

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.

  1. // Subdocument
  2. const subdocumentSchema = new mongoose.Schema({
  3. child: new mongoose.Schema({ name: String, age: Number })
  4. });
  5. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
  6. // Nested path
  7. const nestedSchema = new mongoose.Schema({
  8. child: { name: String, age: Number }
  9. });
  10. const Nested = 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.

  1. const doc1 = new Subdoc({});
  2. doc1.child === undefined; // true
  3. doc1.child.name = 'test'; // Throws TypeError: cannot read property...
  4. const doc2 = new Nested({});
  5. doc2.child === undefined; // false
  6. console.log(doc2.child); // Prints 'MongooseDocument { undefined }'
  7. doc2.child.name = 'test'; // Works

Secondly, in Mongoose 5, Document#set() merges when you call it on a nested path, but overwrites when you call it on a subdocument.

  1. const doc1 = new Subdoc({ child: { name: 'Luke', age: 19 } });
  2. doc1.set({ child: { age: 21 } });
  3. doc1.child; // { age: 21 }
  4. const doc2 = new Nested({ child: { name: 'Luke', age: 19 } });
  5. doc2.set({ child: { age: 21 } });
  6. doc2.child; // { name: Luke, age: 21 }

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.

  1. const subdocumentSchema = new mongoose.Schema({
  2. child: new mongoose.Schema({
  3. name: String,
  4. age: {
  5. type: Number,
  6. default: 0
  7. }
  8. })
  9. });
  10. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
  11. // Note that the `age` default has no effect, because `child`
  12. // is `undefined`.
  13. const doc = new Subdoc();
  14. doc.child; // undefined

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

  1. doc.child = {};
  2. // Mongoose applies the `age` default:
  3. 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.

  1. const childSchema = new mongoose.Schema({
  2. name: String,
  3. age: {
  4. type: Number,
  5. default: 0
  6. }
  7. });
  8. const subdocumentSchema = new mongoose.Schema({
  9. child: {
  10. type: childSchema,
  11. default: () => ({})
  12. }
  13. });
  14. const Subdoc = mongoose.model('Subdoc', subdocumentSchema);
  15. // Note that Mongoose sets `age` to its default value 0, because
  16. // `child` defaults to an empty object and Mongoose applies
  17. // defaults to that empty object.
  18. const doc = new Subdoc();
  19. 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.

  1. 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:

  1. const Parent = mongoose.model('Parent');
  2. const parent = new Parent;
  3. // create a comment
  4. parent.children.push({ name: 'Liesl' });
  5. const subdoc = parent.children[0];
  6. console.log(subdoc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
  7. subdoc.isNew; // true
  8. parent.save(function (err) {
  9. if (err) return handleError(err)
  10. console.log('Success!');
  11. });

Subdocs may also be created without adding them to the array by using the create method of MongooseArrays.

  1. const 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.

  1. // Equivalent to `parent.children.pull(_id)`
  2. parent.children.id(_id).remove();
  3. // Equivalent to `parent.child = null`
  4. parent.child.remove();
  5. parent.save(function (err) {
  6. if (err) return handleError(err);
  7. console.log('the subdocs were removed');
  8. });

Parents of Subdocs

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

  1. const schema = new Schema({
  2. docArr: [{ name: String }],
  3. singleNested: new Schema({ name: String })
  4. });
  5. const Model = mongoose.model('Test', schema);
  6. const doc = new Model({
  7. docArr: [{ name: 'foo' }],
  8. singleNested: { name: 'bar' }
  9. });
  10. doc.singleNested.parent() === doc; // true
  11. doc.docArr[0].parent() === doc; // true

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

  1. const schema = new Schema({
  2. level1: new Schema({
  3. level2: new Schema({
  4. test: String
  5. })
  6. })
  7. });
  8. const Model = mongoose.model('Test', schema);
  9. const doc = new Model({ level1: { level2: 'test' } });
  10. doc.level1.level2.parent() === doc; // false
  11. doc.level1.level2.parent() === doc.level1; // true
  12. 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:

  1. const parentSchema = new Schema({
  2. children: [{ name: 'string' }]
  3. });
  4. // Equivalent
  5. const parentSchema = new Schema({
  6. children: [new Schema({ name: 'string' })]
  7. });

Alternate declaration syntax for single nested subdocuments

Unlike document arrays, Mongoose 5 does not convert an objects in schemas into nested schemas. In the below example, nested is a nested path rather than a subdocument.

  1. const schema = new Schema({
  2. nested: {
  3. prop: String
  4. }
  5. });

This leads to some surprising behavior when you attempt to define a nested path with validators or getters/setters.

  1. const schema = new Schema({
  2. nested: {
  3. // Do not do this! This makes `nested` a mixed path in Mongoose 5
  4. type: { prop: String },
  5. required: true
  6. }
  7. });
  8. const schema = new Schema({
  9. nested: {
  10. // This works correctly
  11. type: new Schema({ prop: String }),
  12. required: true
  13. }
  14. });

Surprisingly, declaring nested with an object type makes nested into a path of type Mixed. To instead make Mongoose automatically convert type: { prop: String } into type: new Schema({ prop: String }), set the typePojoToMixed option to false.

  1. const schema = new Schema({
  2. nested: {
  3. // Because of `typePojoToMixed`, Mongoose knows to
  4. // wrap `{ prop: String }` in a `new Schema()`.
  5. type: { prop: String },
  6. required: true
  7. }
  8. }, { typePojoToMixed: false });

Next Up

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