Mongoose 虛擬屬性
在 Mongoose 中,虛擬屬性是一個不會儲存在 MongoDB 中的屬性。虛擬屬性通常用於文件中計算出來的屬性。
您的第一個虛擬屬性
假設您有一個 User
模型。每個使用者都有一個 email
,但您也想要電子郵件的網域。例如,'test@gmail.com' 的網域部分是 '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: 'test@gmail.com' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'
Schema#virtual()
函數會回傳一個 VirtualType
物件。與一般文件屬性不同,虛擬屬性沒有任何基礎值,而且 Mongoose 不會對虛擬屬性進行任何類型轉換。然而,虛擬屬性有getter 和 setter,這使得它們非常適合用於計算屬性,例如上面的 domain
範例。
虛擬屬性的 Setter
您也可以使用虛擬屬性一次設定多個屬性,作為 一般屬性上自訂 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 中的虛擬屬性
預設情況下,當您將文件轉換為 JSON 時,Mongoose 不會包含虛擬屬性。例如,如果您將文件傳遞給 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: '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()
中的虛擬屬性
預設情況下,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或 pre save 中介軟體來設定該屬性。
Populate(填充)
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: 'test@gmail.com' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // 'test@gmail.com'
透過綱要選項的虛擬屬性
虛擬屬性也可以直接在綱要選項中定義,而無需使用 .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: 'test@gmail.com' });
const doc = await BlogPost.findOne().populate('author');
doc.author.email; // 'test@gmail.com'