嵌套文档与子文档操作
嵌套文档与子文档的基本概念
Mongoose中的嵌套文档是指在一个文档中直接嵌入另一个文档的结构。这种设计模式允许将相关数据组织在一起,无需额外的集合或引用。子文档则是嵌套文档的另一种表现形式,通常作为父文档的一个属性存在。
const childSchema = new mongoose.Schema({
name: String,
age: Number
});
const parentSchema = new mongoose.Schema({
children: [childSchema],
address: String
});
在这个例子中,childSchema
被嵌套在parentSchema
中作为子文档数组。Mongoose会为子文档自动创建_id
字段,除非显式禁用。
创建嵌套文档
创建包含嵌套文档的文档有几种方式。最直接的方法是在创建父文档时同时指定子文档内容:
const Parent = mongoose.model('Parent', parentSchema);
const newParent = new Parent({
address: '123 Main St',
children: [
{ name: 'Alice', age: 8 },
{ name: 'Bob', age: 5 }
]
});
也可以先创建子文档实例,再将其添加到父文档中:
const child1 = { name: 'Charlie', age: 10 };
const child2 = { name: 'Diana', age: 7 };
const anotherParent = new Parent({
address: '456 Oak Ave',
children: [child1, child2]
});
查询嵌套文档
查询嵌套文档时,Mongoose提供了多种操作符和方法。最基本的查询可以通过点符号访问嵌套字段:
Parent.find({ 'children.age': { $gt: 6 } }, (err, parents) => {
console.log(parents);
});
使用$elemMatch
可以执行更复杂的查询:
Parent.find({
children: {
$elemMatch: {
age: { $gt: 5 },
name: /^A/
}
}
});
更新嵌套文档
更新嵌套文档有多种方法。可以使用传统的update
操作:
Parent.updateOne(
{ _id: parentId, 'children._id': childId },
{ $set: { 'children.$.age': 9 } },
(err, result) => {
// 处理结果
}
);
Mongoose还提供了更便捷的findOneAndUpdate
:
Parent.findOneAndUpdate(
{ _id: parentId, 'children._id': childId },
{ $inc: { 'children.$.age': 1 } },
{ new: true }
).then(updatedParent => {
console.log(updatedParent);
});
删除嵌套文档
从父文档中删除子文档可以使用$pull
操作符:
Parent.updateOne(
{ _id: parentId },
{ $pull: { children: { _id: childId } } },
(err, result) => {
// 处理结果
}
);
如果要删除所有符合条件的子文档:
Parent.updateMany(
{},
{ $pull: { children: { age: { $lt: 3 } } } },
{ multi: true }
);
嵌套文档的验证
Mongoose会自动应用子文档模式的验证规则。例如,如果子文档模式要求name
字段:
const strictChildSchema = new mongoose.Schema({
name: { type: String, required: true },
age: Number
});
const strictParentSchema = new mongoose.Schema({
children: [strictChildSchema]
});
尝试保存缺少name
的子文档会触发验证错误:
const invalidParent = new Parent({
children: [{ age: 4 }] // 缺少必填的name字段
});
invalidParent.save(err => {
console.log(err); // 验证错误
});
嵌套文档与中间件
可以在嵌套文档上使用Mongoose中间件。例如,保存前的预处理:
childSchema.pre('save', function(next) {
if (this.age < 0) {
this.age = 0;
}
next();
});
这个中间件会在每个子文档保存前执行,确保年龄不为负数。
嵌套文档数组操作
Mongoose提供了对嵌套文档数组的特殊操作方法。push
方法可以添加新子文档:
Parent.findById(parentId, (err, parent) => {
parent.children.push({ name: 'Eva', age: 6 });
parent.save();
});
使用id
方法可以快速查找特定子文档:
const parent = await Parent.findById(parentId);
const child = parent.children.id(childId);
console.log(child);
嵌套文档与填充(Population)的区别
虽然嵌套文档和填充都能建立文档间关系,但工作机制不同。填充使用引用和额外查询:
const refChildSchema = new mongoose.Schema({
name: String,
age: Number
});
const refParentSchema = new mongoose.Schema({
children: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Child' }]
});
相比之下,嵌套文档将所有数据存储在同一个文档中,查询时不需要额外操作。
嵌套文档的性能考量
嵌套文档适合以下场景:
- 子文档数量有限且不会无限增长
- 子文档主要与父文档一起访问
- 需要原子性更新父文档和子文档
不适合的场景:
- 子文档数量可能非常大
- 需要单独查询或更新子文档
- 多个父文档共享相同的子文档
嵌套文档的深度限制
Mongoose默认不限制嵌套深度,但可以自定义验证:
const deepSchema = new mongoose.Schema({
level1: {
level2: {
level3: {
value: String
}
}
}
});
function validateDepth(doc, maxDepth) {
// 实现深度验证逻辑
}
deepSchema.pre('save', function(next) {
if (validateDepth(this, 3)) {
next(new Error('Maximum nesting depth exceeded'));
} else {
next();
}
});
嵌套文档的索引
可以为嵌套字段创建索引以提高查询性能:
parentSchema.index({ 'children.name': 1 });
parentSchema.index({ 'children.age': -1 });
复合索引也同样适用:
parentSchema.index({ 'children.name': 1, 'children.age': -1 });
嵌套文档与原子操作
Mongoose保证对单个文档(包括其嵌套文档)的更新是原子的。例如,可以原子性地更新父文档和子文档:
Parent.findOneAndUpdate(
{ _id: parentId },
{
$set: { address: 'New Address' },
$inc: { 'children.$.age': 1 }
},
{ new: true }
);
嵌套文档的模式设计技巧
设计嵌套文档模式时考虑以下因素:
- 访问模式:哪些字段经常一起查询
- 更新频率:哪些字段经常被单独更新
- 大小限制:MongoDB文档最大16MB
// 好的设计:经常一起访问的数据放在一起
const userSchema = new mongoose.Schema({
profile: {
name: String,
avatar: String,
bio: String
},
preferences: {
theme: String,
notifications: Boolean
}
});
嵌套文档的迁移策略
当需要修改嵌套文档结构时,可以考虑以下迁移方法:
- 批量更新所有文档:
Parent.updateMany(
{},
{ $rename: { 'children.grade': 'children.level' } }
);
- 使用迁移脚本处理复杂情况:
async function migrateChildren() {
const parents = await Parent.find({});
for (const parent of parents) {
parent.children.forEach(child => {
if (child.grade) {
child.level = convertGradeToLevel(child.grade);
delete child.grade;
}
});
await parent.save();
}
}
嵌套文档与事务
在MongoDB事务中操作嵌套文档时,整个文档被视为一个原子单元:
const session = await mongoose.startSession();
session.startTransaction();
try {
const parent = await Parent.findById(parentId).session(session);
parent.children.push(newChild);
await parent.save();
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
嵌套文档的虚拟属性
可以为嵌套文档添加虚拟属性:
childSchema.virtual('ageGroup').get(function() {
if (this.age < 5) return 'Toddler';
if (this.age < 12) return 'Child';
return 'Teen';
});
const parent = await Parent.findById(parentId);
console.log(parent.children[0].ageGroup); // 根据年龄返回分组
嵌套文档的JSON序列化
控制嵌套文档的JSON输出:
childSchema.set('toJSON', {
transform: (doc, ret) => {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
}
});
parentSchema.set('toJSON', {
virtuals: true
});
嵌套文档的默认值
为嵌套文档设置默认值:
const settingsSchema = new mongoose.Schema({
notifications: { type: Boolean, default: true },
theme: { type: String, default: 'light' }
});
const userSchema = new mongoose.Schema({
settings: { type: settingsSchema, default: () => ({}) }
});
嵌套文档的条件验证
根据父文档状态验证子文档:
parentSchema.path('children').validate(function(children) {
if (this.type === 'large_family' && children.length < 3) {
return false;
}
return true;
}, 'Large families must have at least 3 children');
嵌套文档的查询性能优化
优化嵌套文档查询的方法:
- 使用投影只返回需要的字段:
Parent.find({}, 'address children.name');
- 对常用查询条件创建索引:
parentSchema.index({ 'children.age': 1, address: 1 });
- 使用
$slice
限制返回的数组元素:
Parent.findById(parentId, { children: { $slice: 5 } });
嵌套文档与GraphQL
在GraphQL中处理嵌套文档:
const ParentType = new GraphQLObjectType({
name: 'Parent',
fields: () => ({
address: { type: GraphQLString },
children: {
type: new GraphQLList(ChildType),
resolve(parent) {
return parent.children;
}
}
})
});
const ChildType = new GraphQLObjectType({
name: 'Child',
fields: {
name: { type: GraphQLString },
age: { type: GraphQLInt }
}
});
嵌套文档的版本控制
实现嵌套文档的版本控制:
const versionedChildSchema = new mongoose.Schema({
name: String,
age: Number,
version: { type: Number, default: 0 }
});
childSchema.pre('save', function(next) {
if (this.isModified()) {
this.version += 1;
}
next();
});
嵌套文档的并行处理
安全地并行处理嵌套文档:
async function updateChildAge(parentId, childId, newAge) {
const parent = await Parent.findById(parentId);
const child = parent.children.id(childId);
if (!child) throw new Error('Child not found');
child.age = newAge;
await parent.save();
}
嵌套文档的批量插入
高效插入多个嵌套文档:
async function addMultipleChildren(parentId, newChildren) {
await Parent.findByIdAndUpdate(
parentId,
{ $push: { children: { $each: newChildren } } },
{ new: true }
);
}
嵌套文档的引用计数
在嵌套文档中实现引用计数:
const tagSchema = new mongoose.Schema({
name: String,
count: { type: Number, default: 0 }
});
const postSchema = new mongoose.Schema({
tags: [tagSchema]
});
postSchema.pre('save', function(next) {
this.tags.forEach(tag => {
tag.count = this.tags.filter(t => t.name === tag.name).length;
});
next();
});
嵌套文档的类型转换
处理嵌套文档中的类型转换:
childSchema.path('age').set(function(v) {
return parseInt(v, 10) || 0;
});
const parent = new Parent({
children: [{ age: '10' }] // 自动转换为数字10
});
嵌套文档的加密
加密敏感嵌套字段:
const crypto = require('crypto');
const secret = 'my-secret-key';
childSchema.pre('save', function(next) {
if (this.isModified('ssn')) {
const cipher = crypto.createCipher('aes-256-cbc', secret);
this.ssn = cipher.update(this.ssn, 'utf8', 'hex') + cipher.final('hex');
}
next();
});
childSchema.methods.decryptSsn = function() {
const decipher = crypto.createDecipher('aes-256-cbc', secret);
return decipher.update(this.ssn, 'hex', 'utf8') + decipher.final('utf8');
};
嵌套文档的国际化
实现嵌套文档的多语言支持:
const i18nStringSchema = new mongoose.Schema({
en: String,
es: String,
fr: String
});
const productSchema = new mongoose.Schema({
name: i18nStringSchema,
description: i18nStringSchema
});
productSchema.methods.getName = function(lang) {
return this.name[lang] || this.name.en;
};
嵌套文档的审计日志
跟踪嵌套文档的变更历史:
const auditLogSchema = new mongoose.Schema({
timestamp: { type: Date, default: Date.now },
operation: String,
data: mongoose.Schema.Types.Mixed
});
const auditableSchema = new mongoose.Schema({
content: String,
logs: [auditLogSchema]
});
auditableSchema.pre('save', function(next) {
if (this.isModified()) {
this.logs.push({
operation: this.isNew ? 'create' : 'update',
data: this.getChanges()
});
}
next();
});
嵌套文档的缓存策略
优化嵌套文档的缓存:
const cache = new Map();
parentSchema.statics.findByIdWithCache = async function(id) {
if (cache.has(id)) {
return cache.get(id);
}
const parent = await this.findById(id).lean();
cache.set(id, parent);
return parent;
};
parentSchema.post('save', function(doc) {
cache.set(doc._id.toString(), doc.toObject());
});
嵌套文档的测试策略
测试嵌套文档的示例:
describe('Parent Model', () => {
it('should validate child age', async () => {
const parent = new Parent({
children: [{ name: 'Test', age: -1 }]
});
await expect(parent.save()).rejects.toThrow();
});
it('should update nested child', async () => {
const parent = await Parent.create({ /* ... */ });
await Parent.updateOne(
{ _id: parent._id, 'children._id': parent.children[0]._id },
{ $set: { 'children.$.name': 'Updated' } }
);
const updated = await Parent.findById(parent._id);
expect(updated.children[0].name).toBe('Updated');
});
});
嵌套文档的迁移到独立集合
将嵌套文档迁移为独立集合的策略:
async function migrateChildrenToCollection() {
const parents = await Parent.find({});
const Child = mongoose.model('Child', childSchema);
for (const parent of parents) {
const childDocs = parent.children.map(child => ({
...child.toObject(),
parent: parent._id
}));
await Child.insertMany(childDocs);
await Parent.updateOne(
{ _id: parent._id },
{ $set: { children: [] } }
);
}
}
嵌套文档的聚合操作
在聚合管道中处理嵌套文档:
Parent.aggregate([
{ $unwind: '$children' },
{ $match: { 'children.age': { $gte: 5 } } },
{ $group: {
_id: '$_id',
address: { $first: '$address' },
childrenCount: { $sum: 1 }
}}
]);
嵌套文档的地理空间查询
在嵌套文档中使用地理空间查询:
const locationSchema = new mongoose.Schema({
type: { type: String, default: 'Point' },
coordinates: { type: [Number], index: '2dsphere' }
});
const placeSchema = new mongoose.Schema({
name: String,
locations: [locationSchema]
});
placeSchema.index({ 'locations': '2dsphere' });
Place.find({
locations: {
$near: {
$geometry: {
type: 'Point',
coordinates: [longitude, latitude]
},
$maxDistance: 1000
}
}
});
嵌套文档的全文搜索
实现嵌套文档的全文搜索:
parentSchema.index({
'children.name': 'text',
'children.description': 'text'
});
Parent.find(
{ $text: { $search: 'keyword' } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });
嵌套文档的权限控制
基于角色的嵌套文档访问控制:
parentSchema.methods.filterChildrenForRole = function(role) {
if (role === 'admin') {
return this.children;
} else {
return this.children.map(child => ({
name: child.name,
age: child.age
// 隐藏敏感字段
}));
}
};
嵌套文档的乐观并发控制
实现嵌套文档的版本控制:
childSchema.add({
version: { type: Number, default: 0 }
});
childSchema.pre('save', function(next) {
if (this.isModified()) {
this.version += 1;
}
next();
});
async function updateChildWithConflictDetection(parentId, childId, update) {
const parent = await Parent.findById(parentId);
const child = parent.children.id(childId);
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:数据填充(Population)
下一篇:引用与关联查询