Mongoose 虚拟字段(Virtual Fields)是一种强大的功能,允许定义不存储在数据库中的计算字段。虚拟字段可以基于文档的其他字段动态计算值,或者创建文档之间的关联。
基本虚拟字段
定义虚拟字段
javascriptconst personSchema = new Schema({ firstName: String, lastName: String, birthDate: Date }); // 定义 fullName 虚拟字段 personSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); // 定义虚拟字段的 setter personSchema.virtual('fullName').set(function(name) { const parts = name.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; }); const Person = mongoose.model('Person', personSchema); // 使用虚拟字段 const person = new Person({ firstName: 'John', lastName: 'Doe' }); console.log(person.fullName); // "John Doe" person.fullName = 'Jane Smith'; console.log(person.firstName); // "Jane" console.log(person.lastName); // "Smith"
计算字段
javascriptconst productSchema = new Schema({ price: Number, taxRate: { type: Number, default: 0.1 } }); // 计算含税价格 productSchema.virtual('priceWithTax').get(function() { return this.price * (1 + this.taxRate); }); const Product = mongoose.model('Product', productSchema); const product = new Product({ price: 100, taxRate: 0.2 }); console.log(product.priceWithTax); // 120
虚拟字段关联
一对多关联
javascriptconst authorSchema = new Schema({ name: String, email: String }); const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' } }); // 在 Author 模型上添加虚拟字段,获取所有书籍 authorSchema.virtual('books', { ref: 'Book', localField: '_id', foreignField: 'author' }); const Author = mongoose.model('Author', authorSchema); const Book = mongoose.model('Book', bookSchema); // 使用虚拟字段关联 const author = await Author.findById(authorId).populate('books'); console.log(author.books); // Array of books by this author
多对多关联
javascriptconst studentSchema = new Schema({ name: String }); const courseSchema = new Schema({ title: String, students: [{ type: Schema.Types.ObjectId, ref: 'Student' }] }); // 在 Student 模型上添加虚拟字段,获取所有课程 studentSchema.virtual('courses', { ref: 'Course', localField: '_id', foreignField: 'students' }); const Student = mongoose.model('Student', studentSchema); const Course = mongoose.model('Course', courseSchema); // 使用虚拟字段关联 const student = await Student.findById(studentId).populate('courses'); console.log(student.courses); // Array of courses for this student
虚拟字段选项
基本选项
javascriptuserSchema.virtual('profileUrl', { ref: 'Profile', localField: '_id', foreignField: 'user', justOne: true, // 返回单个文档而不是数组 count: false // 返回计数而不是文档 });
条件虚拟字段
javascriptuserSchema.virtual('isAdult').get(function() { return this.age >= 18; }); userSchema.virtual('status').get(function() { if (this.deleted) return 'deleted'; if (this.banned) return 'banned'; return 'active'; });
虚拟字段在 JSON 和查询中的行为
JSON 序列化
默认情况下,虚拟字段不会包含在 JSON 输出中。需要设置 toJSON 选项:
javascriptconst userSchema = new Schema({ firstName: String, lastName: String }, { toJSON: { virtuals: true }, toObject: { virtuals: true } }); userSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); const user = new User({ firstName: 'John', lastName: 'Doe' }); console.log(JSON.stringify(user)); // 包含 fullName
查询虚拟字段
虚拟字段不能直接用于查询,因为它们不存储在数据库中:
javascript// 不支持 User.find({ fullName: 'John Doe' }); // 不会工作 // 解决方案:使用实际字段 User.find({ firstName: 'John', lastName: 'Doe' });
虚拟字段的限制
- 不能用于查询:虚拟字段不能在查询条件中使用
- 不能用于排序:虚拟字段不能用于排序操作
- 不能用于索引:虚拟字段不能创建索引
- 性能考虑:每次访问都会重新计算
- JSON 输出:需要配置才能包含在 JSON 中
实际应用场景
1. 格式化显示
javascriptconst orderSchema = new Schema({ items: [{ name: String, price: Number, quantity: Number }] }); orderSchema.virtual('totalPrice').get(function() { return this.items.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0); });
2. 数据转换
javascriptconst userSchema = new Schema({ birthDate: Date }); userSchema.virtual('age').get(function() { const today = new Date(); const birthDate = new Date(this.birthDate); let age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; });
3. 关联查询
javascriptconst commentSchema = new Schema({ text: String, author: { type: Schema.Types.ObjectId, ref: 'User' }, post: { type: Schema.Types.ObjectId, ref: 'Post' } }); // 在 Post 模型上添加虚拟字段获取评论 postSchema.virtual('comments', { ref: 'Comment', localField: '_id', foreignField: 'post' });
最佳实践
- 用于计算和格式化:虚拟字段适合用于计算值和格式化显示
- 避免复杂计算:复杂的计算可能影响性能
- 合理使用关联:虚拟字段关联可以简化查询逻辑
- 配置 JSON 输出:根据需要配置 toJSON 和 toObject
- 文档清晰:为虚拟字段添加清晰的注释说明其用途