Contents
Mongoose Discriminator is another very useful and powerful yet underused feature of Mongoose. It serves as a means of schema inheritance, allowing you to use multiple models with intersecting schemas on the same underlying MongoDB collection. You can store different entities in one collection, and you will still be able to discriminate between these entities. Unfortunately, the documentation of this feature is poor.
Why use Discriminator?
Suppose there is a need to run ‘campaign’ of different types for your customers:
- SMS campaign
- Campaign via app notification
- Email campaign
The campaign model will have below fields in common for all
|
|
SMS based campaign will have text: { type: String, required: true }
as additional field.
App notification campaign will have below additional fields
|
|
Email campaign will have below additional fields
|
|
Depending upon the type of campaign, the details will be mandatory accordingly. e.g. For SMS type campaign, text
field will be mandatory but for email, subject
, plain_text
and html_text
will be mandatory.
These are the below alternatives to define the schemas for the above use case
Option 1: Separate Schemas
Define separate schemas for each type of campaign namely SMSCampaign
, NotifCampaign
and EmailCampaign
having all the fields needed for each. This way you can enforce schema validations for each type of campaign. But, this is breaking DRY (Do not Repeat Yourself)
design principle. Also, if you have to introduce a new property, say: periodic: {type: Boolean}
common to all then, you have to add in all three schemas and forgetting for any one type will create bugs.
Option 2: Define a mixed
type field
|
|
This will allow to store campaign details for each type but you will loose schema validations and type casting features of mongoose.
Option 3: Mongoose Discriminator
You can define a base campaign schema with common fields and make use of mongoose.discriminator()
function to differentiate between the different campaigns and yet use the single collection.
|
|
By defining the schema as above, you have not only inherited the properties of base campaign but also created different models for campaign types. The data will be stored in the single collection only.
You may have noticed { discriminatorKey: “type” } in the
baseOptions
. If you don’t give this option mongoose will use default discrimonator key “__t”.
The document saved will look like
|
|
The sample code is checked into mongoose-discriminator on github.
Advantages of using discriminator:
- A clear schema definition for each type
- Mongoose validation, type casting and hooks
- Easy population of data
Updating the discriminator key
By default, Mongoose doesn’t let you update the discriminator key. save()
will throw an error if you attempt to update the discriminator key. And findOneAndUpdate()
, updateOne()
, etc. will strip out discriminator key updates.
To update a document’s discriminator key, use findOneAndUpdate()
or updateOne()
with the overwriteDiscriminatorKey
option.
Conclusion
Discriminators in Mongoose are a robust feature that allows for storing comparable documents with varying schema constraints in a single collection. They are extremely useful in scenarios where using a Mixed type
and bypassing validation
is tempting. Specifically, in applications such as event tracking and user permissions, discriminators can be an essential tool.
✨ Thank you for reading and I hope you find it helpful. I sincerely request for your feedback in the comment’s section.