Mongoose 虚拟
¥Mongoose Virtuals
在 Mongoose 中,虚拟属性是不存储在 MongoDB 中的属性。虚拟通常用于文档的计算属性。
¥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
假设你有 User
型号。每个用户都有一个 email
,但你还需要电子邮件的域。例如,“test@gmail.com”的域部分是 'gmail.com'。
¥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'.
下面是使用 virtual 实现 domain
属性的一种方法。你可以使用 Schema#virtual()
功能 在结构上定义虚拟。
¥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);
const doc = await User.create({ email: 'test@gmail.com' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'
Schema#virtual()
函数返回 VirtualType
对象。与普通文档属性不同,虚拟值没有任何底层值,并且 Mongoose 不会对虚拟值进行任何类型强制。然而,虚拟确实有 getter 和 setter,这使得它们非常适合计算属性,如上面的 domain
示例。
¥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
你还可以使用虚拟值一次设置多个属性,作为 普通属性的自定义设置器 的替代方法。例如,假设你有两个字符串属性:firstName
和 lastName
。你可以创建一个虚拟属性 fullName
,让你可以同时设置这两个属性。关键细节是,在虚拟 getter 和 setter 中,this
指的是虚拟所附加到的文档。
¥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'
JSON 中的虚拟值
¥Virtuals in JSON
默认情况下,当你将文档转换为 JSON 时,Mongoose 不包含虚拟值。例如,如果你将文档传递给 Express'res.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.
要在 res.json()
中包含虚拟,你需要将 toJSON
结构选项 设置为 { virtuals: true }
。
¥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
console.log()
中的虚拟
¥Virtuals in console.log()
默认情况下,Mongoose 在 console.log()
输出中不包含虚拟值。要在 console.log()
中包含虚拟对象,你需要将 toObject
结构选项 设置为 { virtuals: true }
,或者在打印对象之前使用 toObject()
。
¥By default, Mongoose does not include virtuals in console.log()
output.
To include virtuals in console.log()
, you need to set the toObject
schema option to { virtuals: true }
, or use toObject()
before printing the object.
console.log(doc.toObject({ virtuals: true }));
精益虚拟化
¥Virtuals with Lean
虚拟是 Mongoose 文档的属性。如果你使用 精益选择,这意味着你的查询返回 POJO,而不是完整的 Mongoose 文档。这意味着如果你使用 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
如果你使用 lean()
来提高性能,但仍然需要虚拟,Mongoose 有一个 正式支持 mongoose-lean-virtuals
插件,可以用虚拟来装饰精益文档。
¥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 虚拟数据不存储在 MongoDB 中,这意味着你无法基于 Mongoose 虚拟数据进行查询。
¥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' }, null, { strictQuery: false });
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 还支持 填充虚拟。填充的虚拟包含来自另一个集合的文档。要定义填充的虚拟,你需要指定:
¥Mongoose also supports populating virtuals. A populated virtual contains documents from another collection. To define a populated virtual, you need to specify:
ref
选项,告诉 Mongoose 从哪个模型填充文档。¥The
ref
option, which tells Mongoose which model to populate documents from.localField
和foreignField
选项。Mongoose 将从ref
中的模型填充文档,其foreignField
与该文档的localField
匹配。¥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: 'test@gmail.com' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // 'test@gmail.com'
通过结构选项进行虚拟
¥Virtuals via schema options
虚拟也可以直接在结构选项中定义,而不必使用 .virtual
:
¥Virtuals can also be defined in the schema-options directly without having to use .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'
虚拟选项也是如此,例如虚拟填充:
¥The same also goes for virtual options, like virtual populate:
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: 'test@gmail.com' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // 'test@gmail.com'
进一步阅读
¥Further Reading