Mongoose 虚拟属性
在 Mongoose 中,虚拟属性是 **不** 存储在 MongoDB 中的属性。虚拟属性通常用于文档上的计算属性。
你的第一个虚拟属性
假设你有一个 User
模型。每个用户都有一个 email
,但你可能还需要邮箱的域名。例如,'[email protected]' 的域名部分是 'gmail.com'。
下面是使用虚拟属性实现 domain
属性的一种方法。你可以在模式上使用 Schema#virtual()
函数 来定义虚拟属性。
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);
const doc = await User.create({ email: '[email protected]' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'
Schema#virtual()
函数返回一个 VirtualType
对象。与普通文档属性不同,虚拟属性没有任何底层值,Mongoose 不会对虚拟属性进行任何类型强制转换。但是,虚拟属性具有 getter 和 setter,这使得它们成为计算属性的理想选择,例如上面的 domain
示例。
虚拟属性的设置器
你还可以使用虚拟属性一次设置多个属性,作为 普通属性上的自定义 setter 的替代方案。例如,假设你有两个字符串属性:firstName
和 lastName
。你可以创建一个虚拟属性 fullName
,它允许你一次设置这两个属性。关键是,在虚拟属性的 getter 和 setter 中,this
指的是附加了该虚拟属性的文档。
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'
JSON 中的虚拟属性
默认情况下,Mongoose 不会在将文档转换为 JSON 时包含虚拟属性。例如,如果你将文档传递给 Express 的 res.json()
函数,则默认情况下 **不会** 包含虚拟属性。
要在 res.json()
中包含虚拟属性,你需要将 toJSON
模式选项 设置为 { 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
console.log()
中的虚拟属性
默认情况下,Mongoose **不会** 在 console.log()
输出中包含虚拟属性。要在 console.log()
中包含虚拟属性,你需要将 toObject
模式选项 设置为 { virtuals: true }
,或者在打印对象之前使用 toObject()
。
console.log(doc.toObject({ virtuals: true }));
使用 Lean 的虚拟属性
虚拟属性是 Mongoose 文档上的属性。如果你使用 lean 选项,这意味着你的查询返回 POJO 而不是完整的 Mongoose 文档。这意味着如果你使用 lean()
,则没有虚拟属性。
const fullDoc = await User.findOne();
fullDoc.domain; // 'gmail.com'
const leanDoc = await User.findOne().lean();
leanDoc.domain; // undefined
如果你出于性能原因使用 lean()
,但仍然需要虚拟属性,Mongoose 有一个 官方支持的 mongoose-lean-virtuals
插件,它可以用虚拟属性装饰 lean 文档。
限制
Mongoose 虚拟属性 **不会** 存储在 MongoDB 中,这意味着你不能基于 Mongoose 虚拟属性进行查询。
// Will **not** find any results, because `domain` is not stored in
// MongoDB.
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strictQuery: false });
doc; // undefined
如果你想根据计算属性进行查询,你应该使用 自定义 setter 或 预保存中间件 设置该属性。
填充
Mongoose 也支持 填充虚拟属性。填充的虚拟属性包含来自另一个集合的文档。要定义填充的虚拟属性,你需要指定
ref
选项,它告诉 Mongoose 从哪个模型填充文档。localField
和foreignField
选项。Mongoose 将填充来自ref
中模型的文档,其foreignField
与该文档的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: '[email protected]' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'
通过模式选项设置虚拟属性
虚拟属性也可以直接在模式选项中定义,而无需使用 .virtual
const userSchema = mongoose.Schema({
firstName: String,
lastName: String
}, {
virtuals: {
// Create a virtual property `fullName` with a getter and setter
fullName: {
get() { return `${this.firstName} ${this.lastName}`; },
set(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'
虚拟属性选项(如虚拟属性填充)也是如此。
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
title: String,
authorId: Number
}, {
virtuals: {
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
author: {
options: {
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]'