ディスクリミネータ

model.discriminator() 関数

ディスクリミネータとは、スキーマ継承の仕組みです。これにより、同じ基本的な MongoDB コレクション上にオーバーラップしたスキーマを持つ複数のモデルを持つことができます。

1 つのコレクションでさまざまなタイプのイベントを追跡したいとします。すべてのイベントにはタイムスタンプがありますが、クリックされたリンクを表すイベントには URL が必要です。model.discriminator() 関数を使用すると、これを実現できます。この関数は 3 つのパラメータ(モデル名、ディスクリミネータスキーマ、およびオプションのキー(デフォルトはモデル名))を取ります。スキーマが基本スキーマとディスクリミネータスキーマの統合であるモデルを返します。

const options = { discriminatorKey: 'kind' };

const eventSchema = new mongoose.Schema({ time: Date }, options);
const Event = mongoose.model('Event', eventSchema);

// ClickedLinkEvent is a special type of Event that has
// a URL.
const ClickedLinkEvent = Event.discriminator('ClickedLink',
  new mongoose.Schema({ url: String }, options));

// When you create a generic event, it can't have a URL field...
const genericEvent = new Event({ time: Date.now(), url: 'google.com' });
assert.ok(!genericEvent.url);

// But a ClickedLinkEvent can
const clickedEvent = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
assert.ok(clickedEvent.url);

ディスクリミネータがイベントモデルのコレクションに保存される

イベントを追跡するための別のディスクリミネータを作成したとします。ここで、新しいユーザーが登録されたかどうかが分かる訳です。これらの SignedUpEvent インスタンスは、一般的なイベントや ClickedLinkEvent インスタンスと同じコレクションに格納されます。

const event1 = new Event({ time: Date.now() });
const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user: 'testuser' });


await Promise.all([event1.save(), event2.save(), event3.save()]);
const count = await Event.countDocuments();
assert.equal(count, 3);

ディスクリミネータキー

Mongoose がさまざまなディスクリミネータモデルの相違を識別する方法は、「ディスクリミネータキー」であり、デフォルトは __t です。Mongoose は __t という文字列パスをスキーマに追加し、このドキュメントがどのディスクリミネータのインスタンスかを追跡するために使用します。

const event1 = new Event({ time: Date.now() });
const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
const event3 = new SignedUpEvent({ time: Date.now(), user: 'testuser' });

assert.ok(!event1.__t);
assert.equal(event2.__t, 'ClickedLink');
assert.equal(event3.__t, 'SignedUp');

ディスクリミネータキーの更新

デフォルトでは、Mongoose ではディスクリミネータキーを更新できません。ディスクリミネータキーの更新を試みると、save() はエラーをスローします。そして、findOneAndUpdate()updateOne() などはディスクリミネータキーの更新を取り除きます。

let event = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
await event.save();

event.__t = 'SignedUp';
// ValidationError: ClickedLink validation failed: __t: Cast to String failed for value "SignedUp" (type string) at path "__t"
  await event.save();

event = await ClickedLinkEvent.findByIdAndUpdate(event._id, { __t: 'SignedUp' }, { new: true });
event.__t; // 'ClickedLink', update was a no-op

ドキュメントのディスクリミネータキーを更新するには、findOneAndUpdate() または updateOne() を以下のように overwriteDiscriminatorKey オプションを設定して使用します。

let event = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
await event.save();

event = await ClickedLinkEvent.findByIdAndUpdate(
  event._id,
  { __t: 'SignedUp' },
  { overwriteDiscriminatorKey: true, new: true }
);
event.__t; // 'SignedUp', updated discriminator key

配列内の埋め込みディスクリミネータ

埋め込みドキュメントの配列にディスクリミネータを定義することもできます。埋め込みディスクリミネータは、さまざまなディスクリミネータタイプが同じコレクションではなく、1 つのドキュメント内にある同じドキュメント配列に格納されるという点で異なります。言い換えると、埋め込みディスクリミネータを使用すると、さまざまなスキーマに一致するサブドキュメントを同じ配列に格納できます。

一般的なベスト プラクティスとして、フックを使用する前に、スキーマでフックをすべて宣言しているかご確認ください。discriminator() をコールした後は、pre()post() はコールしないでください。

const eventSchema = new Schema({ message: String },
  { discriminatorKey: 'kind', _id: false });

const batchSchema = new Schema({ events: [eventSchema] });

// `batchSchema.path('events')` gets the mongoose `DocumentArray`
// For TypeScript, use `schema.path<Schema.Types.DocumentArray>('events')`
const docArray = batchSchema.path('events');

// The `events` array can contain 2 different types of events, a
// 'clicked' event that requires an element id that was clicked...
const clickedSchema = new Schema({
  element: {
    type: String,
    required: true
  }
}, { _id: false });
// Make sure to attach any hooks to `eventSchema` and `clickedSchema`
// **before** calling `discriminator()`.
const Clicked = docArray.discriminator('Clicked', clickedSchema);

// ... and a 'purchased' event that requires the product that was purchased.
const Purchased = docArray.discriminator('Purchased', new Schema({
  product: {
    type: String,
    required: true
  }
}, { _id: false }));

const Batch = db.model('EventBatch', batchSchema);

// Create a new batch of events with different kinds
const doc = await Batch.create({
  events: [
    { kind: 'Clicked', element: '#hero', message: 'hello' },
    { kind: 'Purchased', product: 'action-figure-1', message: 'world' }
  ]
});

assert.equal(doc.events.length, 2);

assert.equal(doc.events[0].element, '#hero');
assert.equal(doc.events[0].message, 'hello');
assert.ok(doc.events[0] instanceof Clicked);

assert.equal(doc.events[1].product, 'action-figure-1');
assert.equal(doc.events[1].message, 'world');
assert.ok(doc.events[1] instanceof Purchased);

doc.events.push({ kind: 'Purchased', product: 'action-figure-2' });

await doc.save();

assert.equal(doc.events.length, 3);

assert.equal(doc.events[2].product, 'action-figure-2');
assert.ok(doc.events[2] instanceof Purchased);

単一ネストの識別子

サブドキュメントの配列で識別子を定義できるのと同じように、単一ネストのサブドキュメントでも識別子を定義できます。

一般的なベスト プラクティスとして、フックを使用する前に、スキーマでフックをすべて宣言しているかご確認ください。discriminator() をコールした後は、pre()post() はコールしないでください。

const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
const schema = Schema({ shape: shapeSchema });

// For TypeScript, use `schema.path<Schema.Types.Subdocument>('shape').discriminator(...)`
schema.path('shape').discriminator('Circle', Schema({ radius: String }));
schema.path('shape').discriminator('Square', Schema({ side: Number }));

const MyModel = mongoose.model('ShapeTest', schema);

// If `kind` is set to 'Circle', then `shape` will have a `radius` property
let doc = new MyModel({ shape: { kind: 'Circle', radius: 5 } });
doc.shape.radius; // 5

// If `kind` is set to 'Square', then `shape` will have a `side` property
doc = new MyModel({ shape: { kind: 'Square', side: 10 } });
doc.shape.side; // 10