辨別器 (Discriminators)
model.discriminator()
函數
辨別器是一種模式繼承機制。它們使您能夠在同一個底層 MongoDB 集合之上,擁有具有重疊模式的多個模型。
假設您想在單一集合中追蹤不同類型的事件。每個事件都會有一個時間戳記,但代表點擊連結的事件應具有 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);
辨別器會儲存到 Event 模型所在的集合中
假設您建立了另一個辨別器來追蹤新使用者註冊的事件。這些 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
的 String 路徑,用於追蹤此文件是哪個辨別器的實例。
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
嵌入在陣列中的辨別器
您也可以在嵌入式文件陣列上定義辨別器。嵌入式辨別器與眾不同,因為不同的辨別器類型會儲存在同一個文件陣列(在文件內),而不是同一個集合中。換句話說,嵌入式辨別器可讓您在同一個陣列中儲存符合不同模式的子文件。
一般來說,最佳實務是確保在您使用模式**之前**,先宣告模式上的任何 hook。在呼叫 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);
單一巢狀辨別器
您也可以在單一巢狀子文件上定義辨別器,類似於您在子文件陣列上定義辨別器的方式。
一般來說,最佳實務是確保在您使用模式**之前**,先宣告模式上的任何 hook。在呼叫 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