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 的替代方案。例如,假設您有兩個字串屬性:firstNamelastName。您可以建立一個虛擬屬性 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 要從哪個模型填充文件。
  • localFieldforeignField 選項。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'

延伸閱讀