Mongoose 子文档(Subdocuments)是嵌套在父文档中的文档,它们可以是单个文档或文档数组。子文档提供了一种组织相关数据的方式,同时保持数据的完整性。
子文档类型
1. 嵌套 Schema(单个子文档)
javascriptconst addressSchema = new Schema({ street: String, city: String, state: String, zipCode: String }); const userSchema = new Schema({ name: String, email: String, address: addressSchema // 单个子文档 }); const User = mongoose.model('User', userSchema); // 创建包含子文档的用户 const user = await User.create({ name: 'John Doe', email: 'john@example.com', address: { street: '123 Main St', city: 'New York', state: 'NY', zipCode: '10001' } });
2. 子文档数组
javascriptconst commentSchema = new Schema({ text: String, author: String, createdAt: { type: Date, default: Date.now } }); const postSchema = new Schema({ title: String, content: String, comments: [commentSchema] // 子文档数组 }); const Post = mongoose.model('Post', postSchema); // 创建包含子文档数组的文章 const post = await Post.create({ title: 'My First Post', content: 'This is my first post', comments: [ { text: 'Great post!', author: 'Alice' }, { text: 'Thanks for sharing', author: 'Bob' } ] });
子文档操作
访问子文档
javascript// 访问单个子文档 const user = await User.findById(userId); console.log(user.address.city); // "New York" // 访问子文档数组 const post = await Post.findById(postId); console.log(post.comments[0].text); // "Great post!"
修改子文档
javascript// 修改单个子文档 const user = await User.findById(userId); user.address.city = 'Los Angeles'; await user.save(); // 修改子文档数组元素 const post = await Post.findById(postId); post.comments[0].text = 'Updated comment'; await post.save();
添加子文档到数组
javascript// 添加新评论 const post = await Post.findById(postId); post.comments.push({ text: 'New comment', author: 'Charlie' }); await post.save(); // 使用 unshift 添加到开头 post.comments.unshift({ text: 'First comment', author: 'Dave' }); await post.save();
删除子文档
javascript// 删除数组中的子文档 const post = await Post.findById(postId); post.comments.splice(1, 1); // 删除第二个评论 await post.save(); // 使用 pull 删除符合条件的子文档 post.comments.pull({ author: 'Alice' }); await post.save();
子文档中间件
子文档级别的中间件
javascriptcommentSchema.pre('save', function(next) { console.log('Saving comment:', this.text); next(); }); commentSchema.post('save', function(doc) { console.log('Comment saved:', doc.text); });
父文档中间件
javascriptpostSchema.pre('save', function(next) { console.log('Saving post with', this.comments.length, 'comments'); next(); });
子文档验证
子文档级别的验证
javascriptconst addressSchema = new Schema({ street: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true, minlength: 2 }, zipCode: { type: String, required: true, match: /^\d{5}$/ } }); // 验证会在保存父文档时自动触发 try { const user = await User.create({ name: 'John', email: 'john@example.com', address: { street: '123 Main St', city: 'New York', state: 'NY', zipCode: '10001' } }); } catch (error) { console.error('Validation error:', error.message); }
子文档方法
为子文档添加方法
javascriptcommentSchema.methods.getFormattedDate = function() { return this.createdAt.toLocaleDateString(); }; const post = await Post.findById(postId); console.log(post.comments[0].getFormattedDate());
为子文档添加静态方法
javascriptcommentSchema.statics.findByAuthor = function(author) { return this.find({ author }); }; // 注意:子文档的静态方法通常不直接使用 // 更常见的是在父文档上定义方法来操作子文档
子文档引用
使用 ObjectId 引用
javascriptconst postSchema = new Schema({ title: String, content: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }] }); const commentSchema = new Schema({ text: String, author: String }); // 使用 populate 获取完整评论 const post = await Post.findById(postId).populate('comments');
子文档 vs 引用
选择指南
使用子文档当:
- 数据总是与父文档一起访问
- 子文档数量有限且相对较小
- 需要原子性更新
- 数据不需要独立查询
使用引用当:
- 子文档可能独立访问
- 子文档数量可能很大
- 需要跨多个文档查询
- 需要更好的性能
javascript// 子文档示例 - 适合少量评论 const postSchema = new Schema({ title: String, comments: [commentSchema] }); // 引用示例 - 适合大量评论 const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }] });
高级用法
子文档数组操作
javascript// 使用 $push 添加元素 await Post.findByIdAndUpdate(postId, { $push: { comments: { $each: [ { text: 'Comment 1', author: 'User1' }, { text: 'Comment 2', author: 'User2' } ], $position: 0 // 添加到开头 } } }); // 使用 $pull 删除元素 await Post.findByIdAndUpdate(postId, { $pull: { comments: { author: 'User1' } } }); // 使用 $set 更新特定元素 await Post.updateOne( { _id: postId, 'comments._id': commentId }, { $set: { 'comments.$.text': 'Updated text' } } );
子文档验证器
javascriptconst postSchema = new Schema({ title: String, comments: [commentSchema] }); // 自定义验证器 postSchema.path('comments').validate(function(comments) { return comments.length <= 100; }, 'Maximum 100 comments allowed'); // 验证子文档属性 postSchema.path('comments').validate(function(comments) { return comments.every(comment => comment.text.length > 0); }, 'All comments must have text');
最佳实践
- 合理选择结构:根据访问模式选择子文档或引用
- 限制数组大小:避免子文档数组过大
- 使用验证:为子文档添加适当的验证规则
- 考虑性能:大型子文档数组可能影响性能
- 使用中间件:利用中间件处理子文档逻辑
- 文档清晰:为子文档 Schema 添加清晰的注释
- 测试覆盖:为子文档操作编写测试