Mongoose 子文档如何使用,有哪些应用场景?
Mongoose 子文档(Subdocuments)是嵌套在父文档中的文档,它们可以是单个文档或文档数组。子文档提供了一种组织相关数据的方式,同时保持数据的完整性。子文档类型1. 嵌套 Schema(单个子文档)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. 子文档数组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' } ]});子文档操作访问子文档// 访问单个子文档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!"修改子文档// 修改单个子文档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();添加子文档到数组// 添加新评论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();删除子文档// 删除数组中的子文档const post = await Post.findById(postId);post.comments.splice(1, 1); // 删除第二个评论await post.save();// 使用 pull 删除符合条件的子文档post.comments.pull({ author: 'Alice' });await post.save();子文档中间件子文档级别的中间件commentSchema.pre('save', function(next) { console.log('Saving comment:', this.text); next();});commentSchema.post('save', function(doc) { console.log('Comment saved:', doc.text);});父文档中间件postSchema.pre('save', function(next) { console.log('Saving post with', this.comments.length, 'comments'); next();});子文档验证子文档级别的验证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);}子文档方法为子文档添加方法commentSchema.methods.getFormattedDate = function() { return this.createdAt.toLocaleDateString();};const post = await Post.findById(postId);console.log(post.comments[0].getFormattedDate());为子文档添加静态方法commentSchema.statics.findByAuthor = function(author) { return this.find({ author });};// 注意:子文档的静态方法通常不直接使用// 更常见的是在父文档上定义方法来操作子文档子文档引用使用 ObjectId 引用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 引用选择指南使用子文档当:数据总是与父文档一起访问子文档数量有限且相对较小需要原子性更新数据不需要独立查询使用引用当:子文档可能独立访问子文档数量可能很大需要跨多个文档查询需要更好的性能// 子文档示例 - 适合少量评论const postSchema = new Schema({ title: String, comments: [commentSchema]});// 引用示例 - 适合大量评论const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]});高级用法子文档数组操作// 使用 $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' } });子文档验证器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');最佳实践合理选择结构:根据访问模式选择子文档或引用限制数组大小:避免子文档数组过大使用验证:为子文档添加适当的验证规则考虑性能:大型子文档数组可能影响性能使用中间件:利用中间件处理子文档逻辑文档清晰:为子文档 Schema 添加清晰的注释测试覆盖:为子文档操作编写测试