Mongoose virtuals are document properties you can get and set but are not saved in MongoDB. These properties are computed whenever you access them.
Virtual properties are used for formatting and combining fields and de-composing a single value into multiple values before storing it in the collection.
A simple use case for Mongoose's virtual property is the full name consisting of first and last names.
Let us say that you have got the following Mongoose schema:
const mongoose = require('mongoose')
const { Schema } = mongoose
// Define a schema
const userSchema = new Schema({
name: {
first: String,
last: String
},
age: Number
})
// Define a model
const User = mongoose.model('User', userSchema)
// Create a document
const alex = new User({
name: { first: 'Alex', last: 'Jones' },
age: 32
})
Now you want to get the full name of alex
, you can do this manually by concatenating the first
and last
name properties:
console.log(`${alex.name.first} ${alex.name.last}`) // Alex Jones
Defining a virtual property
Instead of combining the first and last name every time, a better approach would be to define a virtual property called fullName
. Mongoose provides two types of virtual methods: GET
and SET
.
GET Method
Virtual get methods are useful for formatting and combining document fields. You can also do any other complex processing before returning a value:
// Define setter virtual method
userSchema.virtual('fullName').get(function () {
return `${this.name.first} ${this.name.last}`
})
Now whenever you access the fullName
property, Mongoose will invoke your virtual method and return the result:
console.log(alex.fullName) // Alex Jones
SET Method
The setter virtual methods are used to split strings into sub-strings and do other operations.
The following example demonstrates how you can split the name property into first and last name at any whitespace:
// Define setter virtual method
userSchema.virtual('fullName').set(function (name) {
const tokens = name.split(' ')
this.name.first = tokens[0]
this.name.last = tokens[1]
})
Now, whenever you assign a value to the fullName
property, it will be automatically split into first and last names:
const john = new User({
fullName: 'John Doe',
age: 32
})
console.log(john.name.first) // John
console.log(john.name.last) // Doe
Mongoose virtual setters are applied before any other validation. So the above example would still work even if the first
and last
properties were required.
Queries and field selection
Mongoose virtuals are just computed properties and are not available for queries and field selection. For example, you can not use the fullName
property to select a user from the database:
// This query won't return any result
User.findOne({
fullName: 'Alex Jones'
})
.then(alex => console.log(alex))
.catch(err => console.log(err))
Only non-virtual properties work for queries and field selections. Since virtuals are not stored in MongoDB, you can not use them in queries.
Including virtuals in JSON
By default, Mongoose ignores virtual properties when you convert a document into a JSON string using either toJSON()
or toObject()
:
// console.log(john.toJSON());
{
name: { first: 'John', last: 'Doe' },
_id: 5fd9b24943e7fa3e6ecf6a3f,
age: 32
}
However, you can pass { virtuals: true }
to the toJSON()
method to include virtuals properties as well:
// console.log(john.toJSON({ virtuals: true }))
{
name: { first: 'John', last: 'Doe' },
_id: 5fd9b24943e7fa3e6ecf6a3f,
age: 32,
fullName: 'John Doe',
id: '5fd9b24943e7fa3e6ecf6a3f'
}
Alternatively, you could also configure Mongoose to automatically include virtual properties for all documents:
mongoose.set('toJSON', { virtuals: true })
This is another reason you should always use virtuals instead of instance methods in Mongoose if you want to include them in JSON.
Read this article to learn more about Mongoose schemas.
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.