乐闻世界logo
搜索文章和话题

Mongoose 子文档如何使用,有哪些应用场景?

2月22日 20:12

Mongoose 子文档(Subdocuments)是嵌套在父文档中的文档,它们可以是单个文档或文档数组。子文档提供了一种组织相关数据的方式,同时保持数据的完整性。

子文档类型

1. 嵌套 Schema(单个子文档)

javascript
const 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. 子文档数组

javascript
const 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();

子文档中间件

子文档级别的中间件

javascript
commentSchema.pre('save', function(next) { console.log('Saving comment:', this.text); next(); }); commentSchema.post('save', function(doc) { console.log('Comment saved:', doc.text); });

父文档中间件

javascript
postSchema.pre('save', function(next) { console.log('Saving post with', this.comments.length, 'comments'); next(); });

子文档验证

子文档级别的验证

javascript
const 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); }

子文档方法

为子文档添加方法

javascript
commentSchema.methods.getFormattedDate = function() { return this.createdAt.toLocaleDateString(); }; const post = await Post.findById(postId); console.log(post.comments[0].getFormattedDate());

为子文档添加静态方法

javascript
commentSchema.statics.findByAuthor = function(author) { return this.find({ author }); }; // 注意:子文档的静态方法通常不直接使用 // 更常见的是在父文档上定义方法来操作子文档

子文档引用

使用 ObjectId 引用

javascript
const 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' } } );

子文档验证器

javascript
const 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');

最佳实践

  1. 合理选择结构:根据访问模式选择子文档或引用
  2. 限制数组大小:避免子文档数组过大
  3. 使用验证:为子文档添加适当的验证规则
  4. 考虑性能:大型子文档数组可能影响性能
  5. 使用中间件:利用中间件处理子文档逻辑
  6. 文档清晰:为子文档 Schema 添加清晰的注释
  7. 测试覆盖:为子文档操作编写测试
标签:Mongoose