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 的替代方案。例如,假设你有两个字符串属性: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 中的虚拟属性

默认情况下,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 从哪个模型填充文档。
  • 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: '[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]'

进一步阅读