子文档
¥Subdocuments
子文档是嵌入其他文档中的文档。在 Mongoose 中,这意味着你可以将结构嵌套在其他结构中。Mongoose 有两种不同的子文档概念:子文档数组 和单个嵌套子文档。
¥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 = new Schema({ name: 'string' });
const parentSchema = new Schema({
// Array of subdocuments
children: [childSchema],
// Single nested subdocuments
child: childSchema
});
请注意,填充的文档不是 Mongoose 中的子文档。子文档数据嵌入在顶层文档中。引用的文档是单独的顶层文档。
¥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 = 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;
什么是子文档?
¥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.
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();
子文档与顶层文档一样有 save
和 validate
中间件。在父文档上调用 save()
会触发其所有子文档的 save()
中间件,对于 validate()
中间件也是如此。
¥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();
});
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()
之前的验证实际上是一个内置的中间件。
¥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
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();
});
子文档与嵌套路径
¥Subdocuments versus Nested Paths
在 Mongoose 中,嵌套路径与子文档略有不同。例如,下面是两个结构:一种将 child
作为子文档,另一种将 child
作为嵌套路径。
¥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.
// 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 特有的差异:
¥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:
首先,Nested
的实例永远不会有 child === undefined
。即使不设置 child
属性,你始终可以设置 child
的子属性。但 Subdoc
的实例可以有 child === undefined
。
¥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 = 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
子文档默认值
¥Subdocument Defaults
默认情况下,子文档路径是未定义的,除非你将子文档路径设置为非空值,否则 Mongoose 不会应用子文档默认值。
¥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
}
})
});
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
默认值。
¥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 递归地应用默认值,这意味着如果你想确保 Mongoose 应用子文档默认值,有一个很好的解决方法:使子文档路径默认为空对象。
¥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: () => ({})
}
});
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 }
查找子文档
¥Finding a Subdocument
每个子文档默认都有一个 _id
。Mongoose 文档数组有一个特殊的 id 方法,用于搜索文档数组以查找具有给定 _id
的文档。
¥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 方法(例如 push
、unshift
、addToSet
等)将参数透明地转换为其正确类型:
¥MongooseArray methods such as push
, unshift
, addToSet
, and others cast arguments to their proper types transparently:
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()
方法 创建子文档而不将其添加到数组中。
¥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
每个子文档都有自己的 deleteOne 方法。对于数组子文档,这相当于在子文档上调用 .pull()
。对于单个嵌套子文档,deleteOne()
相当于将子文档设置为 null
。
¥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
有时,你需要获取子文档的父文档。你可以使用 parent()
函数访问父级。
¥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
如果你有深度嵌套的子文档,则可以使用 ownerDocument()
函数访问顶层文档。
¥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
如果你使用对象数组创建结构,Mongoose 会自动将对象转换为结构:
¥If you create a schema with an array of objects, Mongoose will automatically convert the object to a schema for you:
const parentSchema = new Schema({
children: [{ name: 'string' }]
});
// Equivalent
const parentSchema = new Schema({
children: [new Schema({ name: 'string' })]
});
下一步
¥Next Up
现在我们已经介绍了子文档,让我们看一下 查询。
¥Now that we've covered Subdocuments, let's take a look at querying.