Polymorphic association in Sequelize with migration

coding lollypop
3 min readJan 2, 2021

Understanding why we need polymorphic association is important before implementing it. Imagine three models Video, Image, and Comment. A comment can be posted on a video or an image. So a comment belongs to either one Video or one Comment at a time.

Basic models

Let us make the associations between the above models like so.

Image.hasMany(Comment)
Video.hasMany(Comment)
Comment.belongsTo(Image)
Comment.belongsTo(Video)

This creates two new foreign key columns in the Comment table as you can see below.

Multiple hasMany associations

This violates our requirement as we only want either a video or an image to own comment at a point in time. A comment can only belong to either a video or an image and not both at the same time.

This is why we use polymorphic associations. To enable this we add two columns to the comment table:

  • commentableId : videoId or imageId
  • commentableType: video or image

Sequelize has support for the polymorphic association. To set up we simply use scopes during association.

//video model association
Video.hasMany(models.Comment, {
foreignKey: 'commentableId',
constraints: false,
scope: {
commentableType: 'video'
}
});
//image model association
Image.hasMany(models.Comment, {
foreignKey: 'commentableId',
constraints: false,
scope: {
commentableType: 'image'
}
});
//comment model association
Comment.belongsTo(models.Image, {
foreignKey: 'commentableId',
constraints: false
});
Comment.belongsTo(models.Video, {
foreignKey: 'commentableId',
constraints: false
});

Once the associations are set we can make better use of sequelize mixin functions generated for the associations we made before.

const image = await Image.findByPk(1)
// Sequelize creates a new comment with commentableType as image and commentableId as 1
await image.createComment({title:'Believe in magic'})
// to get all comments belonging to this image.
await image.getComments()

A similar thing can be done for video. If you want to get the video or image model from a comment then we need to implement getCommentable() function in our comments model.

getCommentable(options) {
if (!this.commentableType) return Promise.resolve(null);
const mixinMethodName = `get${uppercaseFirst(this.commentableType)}`;
return this[mixinMethodName](options);
}

This allows us to get the commentable(video or image) from any given comment

const comment1 = await Comment.findByPk(1);
const videoOrImage = comment1.getCommentable();

To directly include video or image from a comment query using eager loading we have to manually add an afterFind hook to avoid duplicates.

//Added to prevent duplicate issues during eager loading.
Comment.addHook("afterFind", findResult => {
if (!Array.isArray(findResult)) findResult = [findResult];
for (const instance of findResult) {
if (instance.commentableType === "image" && instance.Image !== undefined) {
instance.commentable = instance.Image;
} else if (instance.commentableType === "video" && instance.Video !== undefined) {
instance.commentable = instance.Video;
}
// delete to prevent duplicates
delete instance.Image;
delete instance.dataValues.Image;
delete instance.Video;
delete instance.dataValues.Video;
}
});

This allows us to do an eager query like so:

const comments = await Comment.findAll({
include: [Image, Video]
});
for (const comment of comments) {
const message = `Found comment #${comment.id} with ${comment.commentableType} commentable:`;
console.log(message, JSON.stringify(comment.commentable));
}

I have added a working sample app with the above changes and the migration files on this Github project. https://github.com/amalChandran/sequelize-polymorphic-association

--

--