子文档

子文档是嵌入在其他文档中的文档。在 Mongoose 中,这意味着您可以在其他模式中嵌套模式。Mongoose 对子文档有两个不同的概念:子文档数组 和单个嵌套子文档。

const childSchema = new Schema({ name: 'string' });

const parentSchema = new Schema({
  // Array of subdocuments
  children: [childSchema],
  // Single nested subdocuments
  child: childSchema
});

请注意,填充的文档 **不是** Mongoose 中的子文档。子文档数据嵌入在顶级文档中。引用的文档是独立的顶级文档。

const childSchema = new Schema({ name: 'string' });
const Child = mongoose.model('Child', childSchema);

const parentSchema = new Schema({
  child: {
    type: mongoose.ObjectId,
    ref: 'Child'
  }
});
const Parent = mongoose.model('Parent', parentSchema);

const doc = await Parent.findOne().populate('child');
// NOT a subdocument. `doc.child` is a separate top-level document.
doc.child;

什么是子文档?

子文档类似于普通文档。嵌套模式可以有 中间件自定义验证逻辑、虚拟属性,以及顶级模式可以使用的任何其他功能。主要区别在于子文档不是单独保存的,它们在它们的顶级父文档保存时被保存。

const Parent = mongoose.model('Parent', parentSchema);
const 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.
await parent.save();

子文档具有与顶级文档一样的 savevalidate 中间件。在父文档上调用 save() 会触发其所有子文档的 save() 中间件,validate() 中间件也是如此。

childSchema.pre('save', function(next) {
  if ('invalid' == this.name) {
    return next(new Error('#sadpanda'));
  }
  next();
});

const parent = new Parent({ children: [{ name: 'invalid' }] });
try {
  await parent.save();
} catch (err) {
  err.message; // '#sadpanda'
}

子文档的 pre('save')pre('validate') 中间件在顶级文档的 pre('save') **之前** 执行,但在顶级文档的 pre('validate') 中间件 **之后** 执行。这是因为在 save() 之前进行验证实际上是内置中间件的一部分。

// Below code will print out 1-4 in order
const 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();
});

子文档与嵌套路径

在 Mongoose 中,嵌套路径与子文档略有不同。例如,下面是两个模式:一个将 child 作为子文档,另一个将 child 作为嵌套路径。

// Subdocument
const subdocumentSchema = new mongoose.Schema({
  child: new mongoose.Schema({ name: String, age: Number })
});
const Subdoc = mongoose.model('Subdoc', subdocumentSchema);

// Nested path
const nestedSchema = new mongoose.Schema({
  child: { name: String, age: Number }
});
const Nested = mongoose.model('Nested', nestedSchema);

这两个模式看起来很像,并且 MongoDB 中的文档将使用这两个模式具有相同的结构。但是,Mongoose 有几个特定的差异。

首先,Nested 的实例永远不会有 child === undefined。您始终可以设置 child 的子属性,即使您没有设置 child 属性。但是,Subdoc 的实例可以有 child === undefined

const doc1 = new Subdoc({});
doc1.child === undefined; // true
doc1.child.name = 'test'; // Throws TypeError: cannot read property...

const doc2 = new Nested({});
doc2.child === undefined; // false
console.log(doc2.child); // Prints 'MongooseDocument { undefined }'
doc2.child.name = 'test'; // Works

子文档默认值

子文档路径默认情况下是未定义的,除非您将子文档路径设置为非空值,否则 Mongoose 不会应用子文档默认值。

const subdocumentSchema = new mongoose.Schema({
  child: new mongoose.Schema({
    name: String,
    age: {
      type: Number,
      default: 0
    }
  })
});
const Subdoc = mongoose.model('Subdoc', subdocumentSchema);

// Note that the `age` default has no effect, because `child`
// is `undefined`.
const doc = new Subdoc();
doc.child; // undefined

但是,如果您将 doc.child 设置为任何对象,Mongoose 将在必要时应用 age 默认值。

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

Mongoose 递归地应用默认值,这意味着如果您想确保 Mongoose 应用子文档默认值,有一个很好的解决方法:将子文档路径默认为一个空对象。

const childSchema = new mongoose.Schema({
  name: String,
  age: {
    type: Number,
    default: 0
  }
});
const subdocumentSchema = new mongoose.Schema({
  child: {
    type: childSchema,
    default: () => ({})
  }
});
const Subdoc = 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 = new Subdoc();
doc.child; // { age: 0 }

查找子文档

每个子文档都有一个 _id 作为默认值。Mongoose 文档数组有一个特殊的 id 方法,用于在文档数组中搜索以查找具有给定 _id 的文档。

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

将子文档添加到数组

MongooseArray 方法(如 pushunshiftaddToSet 等)会将参数透明地转换为其适当的类型。

const Parent = mongoose.model('Parent');
const parent = new Parent();

// create a comment
parent.children.push({ name: 'Liesl' });
const subdoc = parent.children[0];
console.log(subdoc); // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
subdoc.isNew; // true

await parent.save();
console.log('Success!');

您还可以通过使用 create() 方法 来创建子文档,而不将其添加到数组中。

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

删除子文档

每个子文档都有自己的 deleteOne 方法。对于数组子文档,这等同于在子文档上调用 .pull()。对于单个嵌套子文档,deleteOne() 等同于将子文档设置为 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');

子文档的父级

有时,您需要获取子文档的父级。您可以使用 parent() 函数访问父级。

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

如果您有一个深度嵌套的子文档,您可以使用 ownerDocument() 函数访问顶级文档。

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

数组的替代声明语法

如果您创建了一个包含对象数组的模式,Mongoose 会自动为您将对象转换为模式。

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

下一步

现在我们已经介绍了子文档,让我们看看 查询