Mongoose Virtuals
In Mongoose, a virtual is a property that is not stored in MongoDB. Virtuals are typically used for computed properties on documents.
- Your First Virtual
- Virtual Setters
- Virtuals in JSON
- Virtuals with Lean
- Limitations
- Populate
- Further Reading
Your First Virtual
Suppose you have a User
model. Every user has an email
, but you also want the email's domain. For example, the domain portion of '[email protected]' is 'gmail.com'.
Below is one way to implement the domain
property using a virtual. You define virtuals on a schema using the Schema#virtual()
function.
const userSchema = mongoose.Schema({
email: String
});
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);
let doc = await User.create({ email: '[email protected]' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'
The Schema#virtual()
function returns a VirtualType
object. Unlike normal document properties, virtuals do not have any underlying value and Mongoose does not do any type coercion on virtuals. However, virtuals do have getters and setters, which make them ideal for computed properties, like the domain
example above.
Virtual Setters
You can also use virtuals to set multiple properties at once as an alternative to custom setters on normal properties. For example, suppose you have two string properties: firstName
and lastName
. You can create a virtual property fullName
that lets you set both of these properties at once. The key detail is that, in virtual getters and setters, this
refers to the document the virtual is attached to.
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
});
// Create a virtual property `fullName` with a getter and setter.
userSchema.virtual('fullName').
get(function() { return `${this.firstName} ${this.lastName}`; }).
set(function(v) {
// `v` is the value being set, so use the value to set
// `firstName` and `lastName`.
const firstName = v.substring(0, v.indexOf(' '));
const lastName = v.substring(v.indexOf(' ') + 1);
this.set({ firstName, lastName });
});
const User = mongoose.model('User', userSchema);
const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';
doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'
Virtuals in JSON
By default, Mongoose does not include virtuals when you convert a document to JSON. For example, if you pass a document to Express' res.json()
function, virtuals will not be included by default.
To include virtuals in res.json()
, you need to set the toJSON
schema option to { virtuals: true }
.
const opts = { toJSON: { virtuals: true } };
const userSchema = mongoose.Schema({
_id: Number,
email: String
}, opts);
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);
const doc = new User({ _id: 1, email: '[email protected]' });
doc.toJSON().domain; // 'gmail.com'
// {"_id":1,"email":"[email protected]","domain":"gmail.com","id":"1"}
JSON.stringify(doc);
// To skip applying virtuals, pass `virtuals: false` to `toJSON()`
doc.toJSON({ virtuals: false }).domain; // undefined
Virtuals with Lean
Virtuals are properties on Mongoose documents. If you use the lean option, that means your queries return POJOs rather than full Mongoose documents. That means no virtuals if you use lean()
.
const fullDoc = await User.findOne();
fullDoc.domain; // 'gmail.com'
const leanDoc = await User.findOne().lean();
leanDoc.domain; // undefined
If you use lean()
for performance, but still need virtuals, Mongoose has an officially supported mongoose-lean-virtuals
plugin that decorates lean documents with virtuals.
Limitations
Mongoose virtuals are not stored in MongoDB, which means you can't query based on Mongoose virtuals.
// Will **not** find any results, because `domain` is not stored in
// MongoDB.
const doc = await User.findOne({ domain: 'gmail.com' });
doc; // undefined
If you want to query by a computed property, you should set the property using a custom setter or pre save middleware.
Populate
Mongoose also supports populating virtuals. A populated virtual contains documents from another collection. To define a populated virtual, you need to specify:
- The
ref
option, which tells Mongoose which model to populate documents from. - The
localField
andforeignField
options. Mongoose will populate documents from the model inref
whoseforeignField
matches this document'slocalField
.
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
title: String,
authorId: Number
});
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
blogPostSchema.virtual('author', {
ref: 'User',
localField: 'authorId',
foreignField: '_id',
justOne: true
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);
await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: '[email protected]' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'
Further Reading
- Virtuals in Mongoose Schemas
- Populate Virtuals
- Mongoose Lean Virtuals plugin
- Getting Started With Mongoose Virtuals
- Understanding Virtuals in Mongoose
© 2010 LearnBoost
Licensed under the MIT License.
https://mongoosejs.com/docs/tutorials/virtuals.html