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

你还可以使用虚拟值一次设置多个属性,作为 普通属性的自定义设置器 的替代方法。例如,假设你有两个字符串属性:firstNamelastName。你可以创建一个虚拟属性 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.

  • localFieldforeignField 选项。Mongoose 将从 ref 中的模型填充文档,其 foreignField 与该文档的 localField 匹配。

    ¥The localField and foreignField options. Mongoose will populate documents from the model in ref whose foreignField matches this document's 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'

通过结构选项进行虚拟

¥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