Mongoose バーチャル

Mongooseでは、バーチャルはMongoDBに**保存されない**プロパティです。バーチャルは通常、ドキュメントの計算されたプロパティに使用されます。

初めてのバーチャル

Userモデルがあるとします。すべてのユーザーにはemailがありますが、メールのドメインも必要です。たとえば、'test@gmail.com'のドメイン部分は 'gmail.com' です。

以下は、バーチャルを使用してdomainプロパティを実装する1つの方法です。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はバーチャルに対して型強制を行いません。ただし、バーチャルにはゲッターとセッターがあり、上記のdomainの例のように、計算されたプロパティに最適です。

バーチャルセッター

通常のプロパティのカスタムセッターの代わりに、バーチャルを使用して複数のプロパティを一度に設定することもできます。たとえば、firstNamelastNameという2つの文字列プロパティがあるとします。これらの両方のプロパティを一度に設定できるバーチャルプロパティfullNameを作成できます。重要な詳細は、バーチャルゲッターとセッターでは、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: '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オプションを使用する場合、クエリは完全なMongooseドキュメントではなくPOJOを返します。つまり、lean()を使用すると、バーチャルは存在しません。

const fullDoc = await User.findOne();
fullDoc.domain; // 'gmail.com'

const leanDoc = await User.findOne().lean();
leanDoc.domain; // undefined

パフォーマンスのためにlean()を使用する必要があるが、バーチャルも必要な場合は、Mongooseには、バーチャルでleanドキュメントを装飾する公式にサポートされているmongoose-lean-virtualsプラグインがあります。

制限事項

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

計算されたプロパティでクエリを実行する場合は、カスタムセッターまたはプリセーブミドルウェアを使用してプロパティを設定する必要があります。

Populate

MongooseはバーチャルのPopulateもサポートしています。Populateされたバーチャルには、別のコレクションのドキュメントが含まれています。Populateされたバーチャルを定義するには、以下を指定する必要があります。

  • refオプション。ドキュメントをPopulateするモデルをMongooseに指示します。
  • localFieldおよびforeignFieldオプション。Mongooseは、foreignFieldがこのドキュメントのlocalFieldと一致するrefのモデルからドキュメントをPopulateします。
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'

バーチャル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'

参考文献