Mongoose Virtual Fields are a powerful feature that allows defining computed fields that are not stored in the database. Virtual fields can dynamically calculate values based on other fields in the document, or create associations between documents.
Basic Virtual Fields
Defining Virtual Fields
javascriptconst personSchema = new Schema({ firstName: String, lastName: String, birthDate: Date }); // Define fullName virtual field personSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); // Define virtual field 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); // Use virtual field 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"
Computed Fields
javascriptconst productSchema = new Schema({ price: Number, taxRate: { type: Number, default: 0.1 } }); // Calculate price with tax 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
Virtual Field Associations
One-to-Many Association
javascriptconst authorSchema = new Schema({ name: String, email: String }); const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' } }); // Add virtual field to Author model to get all books authorSchema.virtual('books', { ref: 'Book', localField: '_id', foreignField: 'author' }); const Author = mongoose.model('Author', authorSchema); const Book = mongoose.model('Book', bookSchema); // Use virtual field association const author = await Author.findById(authorId).populate('books'); console.log(author.books); // Array of books by this author
Many-to-Many Association
javascriptconst studentSchema = new Schema({ name: String }); const courseSchema = new Schema({ title: String, students: [{ type: Schema.Types.ObjectId, ref: 'Student' }] }); // Add virtual field to Student model to get all courses studentSchema.virtual('courses', { ref: 'Course', localField: '_id', foreignField: 'students' }); const Student = mongoose.model('Student', studentSchema); const Course = mongoose.model('Course', courseSchema); // Use virtual field association const student = await Student.findById(studentId).populate('courses'); console.log(student.courses); // Array of courses for this student
Virtual Field Options
Basic Options
javascriptuserSchema.virtual('profileUrl', { ref: 'Profile', localField: '_id', foreignField: 'user', justOne: true, // Return single document instead of array count: false // Return count instead of documents });
Conditional Virtual Fields
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'; });
Virtual Field Behavior in JSON and Queries
JSON Serialization
By default, virtual fields are not included in JSON output. Need to set toJSON option:
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)); // Includes fullName
Querying Virtual Fields
Virtual fields cannot be used directly in queries because they are not stored in the database:
javascript// Not supported User.find({ fullName: 'John Doe' }); // Won't work // Solution: Use actual fields User.find({ firstName: 'John', lastName: 'Doe' });
Virtual Field Limitations
- Cannot be used in queries: Virtual fields cannot be used in query conditions
- Cannot be used for sorting: Virtual fields cannot be used for sorting operations
- Cannot be indexed: Virtual fields cannot create indexes
- Performance considerations: Recalculated every time accessed
- JSON output: Requires configuration to be included in JSON
Practical Use Cases
1. Formatting Display
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. Data Transformation
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. Association Queries
javascriptconst commentSchema = new Schema({ text: String, author: { type: Schema.Types.ObjectId, ref: 'User' }, post: { type: Schema.Types.ObjectId, ref: 'Post' } }); // Add virtual field to Post model to get comments postSchema.virtual('comments', { ref: 'Comment', localField: '_id', foreignField: 'post' });
Best Practices
- Use for calculation and formatting: Virtual fields are suitable for calculated values and formatted display
- Avoid complex calculations: Complex calculations may affect performance
- Reasonable use of associations: Virtual field associations can simplify query logic
- Configure JSON output: Configure toJSON and toObject as needed
- Clear documentation: Add clear comments explaining the purpose of virtual fields