diff --git a/docs/guides.pug b/docs/guides.pug index 895f60b9eca..0e94d655c53 100644 --- a/docs/guides.pug +++ b/docs/guides.pug @@ -39,6 +39,7 @@ block content * [Query Casting](/docs/tutorials/query_casting.html) * [findOneAndUpdate](/docs/tutorials/findoneandupdate.html) * [Getters and Setters](/docs/tutorials/getters-setters.html) + * [Virtuals](/docs/tutorials/virtuals.html) ### Integrations diff --git a/docs/tutorials/custom-casting.html b/docs/tutorials/custom-casting.html index 2269f7284a2..627dfd87cf3 100644 --- a/docs/tutorials/custom-casting.html +++ b/docs/tutorials/custom-casting.html @@ -1,5 +1,5 @@ -Mongoose v5.6.1-pre: Mongoose Tutorials: Custom Casting

Custom Casting

+Mongoose v5.6.4-pre: Mongoose Tutorials: Custom Casting

Custom Casting

Working With Dates

+Mongoose v5.6.4-pre: Mongoose Tutorials: Working With Dates

Working With Dates

How to Use findOneAndUpdate() in Mongoose

+Mongoose v5.6.4-pre: Mongoose Tutorials: How to Use `findOneAndUpdate()` in Mongoose

How to Use findOneAndUpdate() in Mongoose

Getters/Setters in Mongoose

+Mongoose v5.6.4-pre: Mongoose Tutorials: Getters/Setters in Mongoose

Getters/Setters in Mongoose

Faster Mongoose Queries With Lean

+Mongoose v5.6.4-pre: Mongoose Tutorials: Faster Mongoose Queries With Lean

Faster Mongoose Queries With Lean

Query Casting

+Mongoose v5.6.4-pre: Mongoose Tutorials: Query Casting

Query Casting

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

+

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 +'test@gmail.com' 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: 'test@gmail.com' });
+// `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: 'test@gmail.com' });
+
+doc.toJSON().domain; // 'gmail.com'
+// {"_id":1,"email":"test@gmail.com","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 and foreignField options. Mongoose will populate documents from the model in ref whose foreignField matches this document's localField.
  • +
+
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: 'test@gmail.com' });
+
+const doc = await BlogPost.findOne().populate('author');
+doc.author.email; // 'test@gmail.com'
+

Further Reading

+ +
\ No newline at end of file diff --git a/docs/tutorials/virtuals.md b/docs/tutorials/virtuals.md new file mode 100644 index 00000000000..5a3052e189c --- /dev/null +++ b/docs/tutorials/virtuals.md @@ -0,0 +1,106 @@ +# 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](#your-first-virtual) +* [Virtual Setters](#virtual-setters) +* [Virtuals in JSON](#virtuals-in-json) +* [Virtuals with Lean](#virtuals-with-lean) +* [Limitations](#limitations) +* [Populate](#populate) +* [Further Reading](#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 +'test@gmail.com' 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](/docs/api/schema.html#schema_Schema-virtual). + +```javascript +[require:Virtuals.*basic] +``` + +The `Schema#virtual()` function returns a [`VirtualType` object](/docs/api/virtualtype.html). 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](/docs/tutorials/getters-setters.html), 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](/docs/tutorials/getters-setters.html#setters). 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. + +```javascript +[require:Virtuals.*fullName] +``` + +## 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](http://expressjs.com/en/4x/api.html#res.json), +virtuals will **not** be included by default. + +To include virtuals in `res.json()`, you need to set the +[`toJSON` schema option](/docs/guide.html#toJSON) to `{ virtuals: true }`. + +```javascript +[require:Virtuals.*toJSON] +``` + +## Virtuals with Lean + +Virtuals are properties on Mongoose documents. If you use the +[lean option](/docs/tutorials/lean.html), that means your queries return POJOs +rather than full Mongoose documents. That means no virtuals if you use +[`lean()`](/docs/api/query.html#query_Query-lean). + +```javascript +[require:Virtuals.*lean] +``` + +If you use `lean()` for performance, but still need virtuals, Mongoose +has an +[officially supported `mongoose-lean-virtuals` plugin](https://plugins.mongoosejs.io/plugins/lean-virtuals) +that decorates lean documents with virtuals. + +## Limitations + +Mongoose virtuals are **not** stored in MongoDB, which means you can't query +based on Mongoose virtuals. + +```javascript +[require:Virtuals.*in query] +``` + +If you want to query by a computed property, you should set the property using +a [custom setter](/docs/tutorials/getters-setters.html) or [pre save middleware](/docs/middleware.html). + +## Populate + +Mongoose also supports [populating virtuals](/docs/populate.html). 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` and `foreignField` options. Mongoose will populate documents from the model in `ref` whose `foreignField` matches this document's `localField`. + +```javascript +[require:Virtuals.*populate] +``` + +## Further Reading + +* [Virtuals in Mongoose Schemas](/docs/guide.html#virtuals) +* [Populate Virtuals](/docs/populate.html#populate-virtuals) +* [Mongoose Lean Virtuals plugin](https://plugins.mongoosejs.io/plugins/lean-virtuals) +* [Getting Started With Mongoose Virtuals](https://masteringjs.io/tutorials/mongoose/virtuals) +* [Understanding Virtuals in Mongoose](https://futurestud.io/tutorials/understanding-virtuals-in-mongoose) \ No newline at end of file diff --git a/test/es-next.test.js b/test/es-next.test.js index 9fe3e0aaae6..bf26b4ca365 100644 --- a/test/es-next.test.js +++ b/test/es-next.test.js @@ -9,4 +9,5 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 8) { require('./es-next/cast.test.es6.js'); require('./es-next/findoneandupdate.test.es6.js'); require('./es-next/getters-setters.test.es6.js'); + require('./es-next/virtuals.test.es6.js'); } diff --git a/test/es-next/virtuals.test.es6.js b/test/es-next/virtuals.test.es6.js new file mode 100644 index 00000000000..959f78496b4 --- /dev/null +++ b/test/es-next/virtuals.test.es6.js @@ -0,0 +1,170 @@ +'use strict'; + +const assert = require('assert'); +const start = require('../common'); + +const mongoose = new start.mongoose.Mongoose(); +const Schema = mongoose.Schema; + +// This file is in `es-next` because it uses async/await for convenience + +describe('Virtuals', function() { + before(async function() { + await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true }); + }); + + beforeEach(function() { + mongoose.deleteModel(/.*/); + }); + + it('basic', async 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: 'test@gmail.com' }); + // `domain` is now a property on User documents. + doc.domain; // 'gmail.com' + // acquit:ignore:start + assert.equal(doc.domain, 'gmail.com'); + // acquit:ignore:end + }); + + it('fullName', async function() { + 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' + // acquit:ignore:start + assert.equal(doc.fullName, 'Jean-Luc Picard'); + assert.equal(doc.firstName, 'Jean-Luc'); + assert.equal(doc.lastName, 'Picard'); + // acquit:ignore:end + }); + + it('toJSON', async function() { + 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: 'test@gmail.com' }); + + doc.toJSON().domain; // 'gmail.com' + // {"_id":1,"email":"test@gmail.com","domain":"gmail.com","id":"1"} + JSON.stringify(doc); + + // To skip applying virtuals, pass `virtuals: false` to `toJSON()` + doc.toJSON({ virtuals: false }).domain; // undefined + // acquit:ignore:start + assert.equal(doc.toJSON().domain, 'gmail.com'); + assert.equal(JSON.stringify(doc), + '{"_id":1,"email":"test@gmail.com","domain":"gmail.com","id":"1"}'); + assert.equal(doc.toJSON({ virtuals: false }).domain, void 0); + // acquit:ignore:end + }); + + it('lean', async function() { + // acquit:ignore:start + 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); + await User.deleteMany({}); + await User.create({ email: 'test@gmail.com' }); + // acquit:ignore:end + const fullDoc = await User.findOne(); + fullDoc.domain; // 'gmail.com' + + const leanDoc = await User.findOne().lean(); + leanDoc.domain; // undefined + // acquit:ignore:start + assert.equal(fullDoc.domain, 'gmail.com'); + assert.equal(leanDoc.domain, void 0); + // acquit:ignore:end + }); + + it('in query', async function() { + // acquit:ignore:start + 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); + await User.deleteMany({}); + await User.create({ email: 'test@gmail.com' }); + // acquit:ignore:end + // Will **not** find any results, because `domain` is not stored in + // MongoDB. + const doc = await User.findOne({ domain: 'gmail.com' }); + doc; // undefined + // acquit:ignore:start + assert.equal(doc, null); + // acquit:ignore:end + }); + + it('populate', async function() { + 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: 'test@gmail.com' }); + + const doc = await BlogPost.findOne().populate('author'); + doc.author.email; // 'test@gmail.com' + // acquit:ignore:start + assert.equal(doc.author.email, 'test@gmail.com'); + // acquit:ignore:end + }); +}); \ No newline at end of file diff --git a/website.js b/website.js index 79f98521503..a0e0376344f 100644 --- a/website.js +++ b/website.js @@ -32,7 +32,8 @@ const tests = [ ...acquit.parse(fs.readFileSync('./test/es-next/cast.test.es6.js').toString()), ...acquit.parse(fs.readFileSync('./test/es-next/findoneandupdate.test.es6.js').toString()), ...acquit.parse(fs.readFileSync('./test/docs/custom-casting.test.js').toString()), - ...acquit.parse(fs.readFileSync('./test/es-next/getters-setters.test.es6.js').toString()) + ...acquit.parse(fs.readFileSync('./test/es-next/getters-setters.test.es6.js').toString()), + ...acquit.parse(fs.readFileSync('./test/es-next/virtuals.test.es6.js').toString()) ]; function getVersion() {