---
title: 'Sequelize Polymorphic Associations'
disqus: hackmd
---
# Polymorphic Associations in Sequelize
# Table of Contents
[TOC]
# What this guide contains
There are two examples here of polymorphic associations in Express with Sequelize. The first will be a One-To-Many style association where the table with a foreign key could belong to one of two other tables. In this case, it will be an Image that could belong to either a Post or a Comment.
The second example is a bit more complex, as it involves a table for Likes. This will entail a Many-to-Many relationship from a User model, through a Likes model, to *either* a Comment model or a Post model on a case-by-case basis.
I will include a database diagram, code snippets, and descriptions for both.
# First Example: `Images` belonging to either `Posts` or `Comments` (One-To-Many)
---
## Database Diagram
This first example is the easier of the two, though there is some code in it that may seem rather odd. Luckily, there isn't much we haven't seen before other than the `Images` table and the associations for it.

The main takeaway from the new Images table is that in place of a single foreign key that can form a connection with an attribute on another table, we have two attributes doing a similar job. `imageableId` will hold the value that is meant to be equivalent to the primary key on another table, and `imageableType` will indicate *which* table is being referenced in the association. Both attributes are necessary for pinpointing which table and which row on that table the Image instance will reference.
With our plan for the app firmly in place, let's take a look at what our migration for `Images` will look like.
## Images migration
An important note is that the migrations for `Users`, `Posts`, and `Comments` will not change based on this new `Images` migration. Run the command to generate a new model and migration together for your `Images` table. Reference the database diagram above for the necessary attributes. If you're using `enum` for the first time, `imageableType:enum,` should suffice for the initial migration CLI command, and you can add in the accepted values for the enum later.
After generating the model and migration, jump into the new migration file. We're just going to make a few small changes to get our migration up and running.
```javascript=
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Images', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
url: {
type: Sequelize.STRING,
allowNull: false,
},
imageableId: {
type: Sequelize.INTEGER,
allowNull: false,
},
imageableType: {
type: Sequelize.ENUM('post', 'comment'),
allowNull: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Images');
}
};
```
Adding `allowNull: false` to attributes is the same as always, just ensuring that any attributes that are vital to each row in the table are always populated. If you haven't seen it before, `defaultValue: Sequelize.fn('now')` is a simple way of adding a default value to each of the date attributes so that you don't have to add them to the seeder file.
Now for this line: `type: Sequelize.ENUM('post', 'comment')`. This line is setting up the attribute `imageableType` to contain a value that is either `'post'` or `'comment'`. Because `Posts` and `Comments` are the two tables that this attribute could potentially reference, we never want `imageableType` to ever contain a different value. With that, we're set! You can run this migration if you like, and then we'll jump over to the model files.
## The models
The models should be nearly set from just generating the migrations. If you didn't generate the models and migrations with the necessary attributes, make sure to fix those here as well. Other than that, the only line you'll need to fix right now is for `imageableType` in the Image model. Since we're using an enum, you'll need to ensure that you add the possible values for that enum, like so:
`imageableType: DataTypes.ENUM('post', 'comment')`
With that, we should be able to move on to setting up the associations, which is the exciting part!
## Setting up relationships
Let's start off with one of the `hasMany` sides of this new relationship. Looking at the Post model, we'll add a relationship to the Image model.
```javascript=
Post.hasMany(models.Image, {
foreignKey: 'imageableId',
constraints: false,
scope: {
imageableType: 'post'
}
});
```
As you can see, we say that `imageableId` is the foreign key for this relationship. However, as we know, that's not the full story. Because the relationship actually relies on both `imageableId` and `imageableType`, we need a bit more code. `constraints: false` allows us to technically reference multiple tables (Posts *and* Comments). The scope of this association sets up a rule that dictates that this association comes into play when the `imageableType` of the instance of Image is 'post'.
The association on the Comment model will be nearly identical to the one in Post, the only differences being that we put `Comment.hasMany(...` instead of `Post.hasMany(...` and we set the `imageableType` of the scope object to `'comment'` instead of `'post'`.
That's realistically all you need! With this, from any instance of Post or Comment, you will be able to access via eager loading any of its images. It's that easy! There's one more association you could add from the *other* side. While it doesn't really seem necessary for this app, you may decide that *your* polymorphic One-to-Many association needs to move in both directions. We'll cover that in the next section.
## Relationship from Image-side
As mentioned previously, this association is meant to be from Image to *either* an associated Post or Comment. This will be a bit more involved than the hasMany associations we just set up, but it's not an outlandish amount of code.
The first bit of code we'll go through is the associations themselves. Here they are:
```javascript=
Image.belongsTo(models.Post, { foreignKey: 'imageableId', constraints: false });
Image.belongsTo(models.Comment, { foreignKey: 'imageableId', constraints: false });
```
This is pretty simple so far. Two fairly standard `belongsTo` associations, with the only detour from the usual being that we add `constraints: false`. Again, that is meant to prevent Sequelize from complaining when we associate Image with multiple tables via the same foreign key.
The next bit of code is a bit more intimidating, but before long it'll make sense:
```javascript=
Image.associate = function(models) {
Image.belongsTo(models.Post, { foreignKey: 'imageableId', constraints: false });
Image.belongsTo(models.Comment, { foreignKey: 'imageableId', constraints: false });
};
Image.addHook("afterFind", findResult => {
if (findResult === null) return;
if (!Array.isArray(findResult)) findResult = [findResult];
for (const instance of findResult) {
if (instance.imageableType === "post" && instance.Post !== undefined) {
instance.dataValues.imageable = instance.Post.dataValues;
} else if (instance.imageableType === "comment" && instance.Comment !== undefined) {
instance.dataValues.imageable = instance.Comment.dataValues;
}
// To prevent mistakes:
delete instance.Post;
delete instance.dataValues.Post;
delete instance.Comment;
delete instance.dataValues.Comment;
}
});
return Image;
```
Well now, isn't that something. So what are we doing here?
To start with, we're adding a hook to Image. This hook is meant to run whenever we query for an instance of Image or several instances of Image. Unfortunately, what it will initially do is grab any potential connections via the associations you just set up. This means that if you have an Image instance with an `imageableId` of `1`, it will grab both the Post and the Comment that have an `id` of `1`. Because of this, we'll have to narrow it down ourselves. We do this by ensuring that we have an iterable to move through, and then iterate over it and check the `imageableType` of each instance. Once it's figured out which one is correct, it will copy the dataValues of the Post/Comment instance over to a new spot in the dataValues of the image. Finally, it will clear out the potential matches from the Image instance, as they are no longer needed.
If you've ever looked at the Sequelize documentation for polymorphic associations, you'll notice that there are a few key differences between their "afterFind" hook and this one. One difference is that since I capitalized my models, I need to capitalize any references to them (like `instance.Post` and `instance.Comment`). Another difference is that instead of just putting the `imageable` key-value pair on the instance of Image (where we would then have to pull it out before sending it back in a response), I placed the dataValues of the `imageable` directly on the dataValues of the instance of Image. With this, I don't need any extra steps when querying for an instance or instances of an Image and sending them to the frontend as JSON.
I also decided to add `if (findResult === null) return;` to the beginning of the function to prevent an error from being thrown when this function is run with a query for an instance of Image that doesn't exist.
If you'd like to see why I made these decisions, you can throw console logs in this function to see how the data is coming in, being packaged, and being sent right back out (`console.log(instance)` should help!).
## Seeding Images and a Route
Seeding images won't be a very involved process. It's not very different from what you already know. Here's a small example of a bulkInsert for `Images`.
```javascript=
return queryInterface.bulkInsert('Images', [
{
url: 'image_1_for_post_1',
imageableId: 1,
imageableType: 'post'
},
{
url: 'image_2_for_post_1',
imageableId: 1,
imageableType: 'post'
},
{
url: 'image_1_for_comment_1',
imageableId: 1,
imageableType: 'comment'
},
{
url: 'image_1_for_comment_2',
imageableId: 2,
imageableType: 'comment'
},
], {});
```
Please note that as always, you'll need your `imageableId` to be an actual `id` on the associated table, and the `imageableType` must be pulled from the acceptable values for your enum.
Now onto a testing route! For this we can just set up a simple route that will take in an `id` via the params and then query for a Post, a Comment, and an Image with that id. Then we send it all back via `res.json({...})` so that we can make sure everything is in good shape via Postman. This would be an excellent time to make sure that your associations are bringing in the *correct* information, especially when it comes to your instance of Image.
```javascript=
router.get(
'/:id',
asyncHandler(async (req, res) => {
const id = req.params.id;
const post = await Post.findByPk(id, {
include: Image
})
const comment = await Comment.findByPk(id, {
include: Image
})
const image = await Image.findByPk(id, {
include: [Post, Comment]
})
return res.json({
post,
comment,
image,
});
})
);
```
If everything looks good after testing your route, you've done it! You've created a Many-to-One polymorphic association in Sequelize. Well done!
# Second Example: A `Likes` table connecting `Users` to both `Posts` and `Comments` (Many-To-Many)
---
## Database Diagram
As you can see with the database diagram below, this process for creating a joins table through which to facilitate a Many-to-Many Polymorphic association is not unlike what we did in the first section of this walkthrough for `Images`.

The only real difference is that now on that table we also have a foreign key that you are likely more used to: `userId`. To reiterate the idea behind the other potential connection, we have two attributes: `likeableId` and `likeableType`. The `likeableId` will hold the integer that we compare to the primary key of either the `Posts` or the `Comments` tables. The `likeableType` is meant to determine which table we're looking at (`Posts` or `Comments`). Rather than having a single attribute to act as the foreign key, it is the combination of these two attributes that allow for a connection between `Likes` and *either* `Posts` *or* `Comments`.
## The migration
As far as migrations go, the ones for `Users`, `Posts`, and `Comments` will not be anything you haven't learned before. It's the migration for `Likes` that will differ in some ways, so we'll focus on that. Go ahead and generate a model and migration for the new `Likes` table. We're going to make a few changes to the migration and end up with something like this:
```javascript=
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Likes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: { model: "Users" },
unique: 'like_unique_constraint',
},
likeableId: {
type: Sequelize.INTEGER,
allowNull: false,
unique: 'like_unique_constraint',
},
likeableType: {
type: Sequelize.ENUM('post','comment'),
allowNull: false,
unique: 'like_unique_constraint',
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
}
},
{
uniqueKeys: {
like_unique_constraint: {
fields: ['userId', 'likeableId', 'likeableType'],
},
},
}
);
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Likes');
}
};
```
Alright, there's a whole bunch of code there. Much of it we're familiar with, some things are new. Let's break down the new stuff a bit.
First and foremost, that new datatype: enum. In the line `type: Sequelize.ENUM('post','comment'),`, we've added `('post', 'comment')` just as we did for the `Images` migration in the first part of this tutorial. What we're saying is that we want to set up an enum that contains the values of `'post'` and `'comment'`, so that this attribute can only be set to one of those two values.
Secondly, we add `defaultValue: Sequelize.fn('now')` to the timestamps to add a default value when an explicit one isn't included. That's just so we don't have to put those date values in our seeders.
Finally, we're setting up a bit of weird code relating to uniqueness. The line `unique: 'like_unique_constraint',` is saying that for the unique constraint we're setting up on that attribute, we want it to rely on a custom setup. That custom setup is the code I've posted below:
```javascript=
{
uniqueKeys: {
like_unique_constraint: {
fields: ['userId', 'likeableId', 'likeableType'],
},
},
}
```
What this code is setting up is a **composite unique key**. So rather than saying that `userId` or `likeableId` have to be unique, we're saying that the *combination* of a `userId`, `likeableId`, and `likeableType` have to be unique. For example, we will never have two instances of a Like where the `userId` is `1`, the `likeableId` is `2` *and* the `likeableType` is `'post'`. So User 1 can never like Post 2 twice.
After we've set that up, we should be good to go check out our models.
## The model
Let's start with the Like model, as it has the least amount of work left to do. The only thing we really need to finish is adding the potential values to the enum, meaning that we'll end up with `likeableType: DataTypes.ENUM('post', 'comment')`.
As far as associations for Like go, we don't actually need any. We're never going to want to start a query at the Like model and eagerload other models with it. Since we have `belongsToMany` associations, we also don't need to include Like in a query to get from User to Post/Comment, or vice versa. If you really want to, you could set up a simple belongsTo based on `userId`, just as you're used to. You could also reference the setup for Image we did in the first part of this tutorial to see code similar to what you'd need to association Like directly with Post and Comment. However, to keep this as brief as possible we won't explicitly outline what that setup looks like here.
## Setting Up Associations Through Likes
Let's look at the fun new associations instead. We'll be setting up a belongsToMany from Post to User first. Here's some code for that association.
```javascript=
Post.belongsToMany(models.User, {
through: {
model: 'Like',
unique: false,
scope: {
likeableType: 'post',
},
},
as: 'likingUsers',
foreignKey: 'likeableId',
constraints: false,
});
```
This is a bit different than the already complex starndard belongsToMany association, though the main points are the same. We're telling Sequelize that this relationship is going *through* another model, in this case Like. The scope signifies that the relationship can only occur if the `likeableType` is `'post'`. We're setting `foreignKey` to `likeableId`, because it will hold the integer that will be equivalent to the `id` on Post. Finally, we have the line `as: 'likingUsers'` because there is potentially another association to User already, since Post has a `userId`.
The lines `unique: false,` and `constraints: false` stop Sequelize from complaining over this being polymorphic in nature.
>Fun Fact: I've tested various associations without those two lines and they appeared to work just fine. Yet I leave them in every time I make a new Many-to-Many polymorphic association because the documentation tells me to and I'm paranoid. At the very least, they don't seem to break anything.
The `belongsToMany` association from Comment to User should be almost exactly the same, except that you will need to change `Post/post` to `Comment/comment` wherever applicable.
Now for the associations on User. Again, let's look at the one concerned with Post.
```javascript=
User.belongsToMany(models.Post, {
through: {
model: 'Like',
unique: false,
scope: {
likeableType: 'post',
},
},
foreignKey: 'userId',
as: 'likedPosts',
constraints: false,
});
```
Okay, this looks very similar! There no new code here, thankfully. Go ahead and set up an association from User to Comment that looks exactly like that, except switching out any `Post/post` with `Comment/comment`.
With that accomplished, let's look at seeding some likes into the table and then querying for information!
## Seeding Likes and a Route
To query, there must be something to query for, so let's throw some seed data into our Likes table. Please keep in mind that you will need to seed some data for the other tables to facilitate the connections your likes will rely on. Here's a sample bulkInsert so you can get the necessary format down.
```javascript=
return queryInterface.bulkInsert('Likes', [
{
userId: 2,
likeableId: 1,
likeableType: 'post'
},
{
userId: 3,
likeableId: 2,
likeableType: 'post'
},
{
userId: 2,
likeableId: 1,
likeableType: 'comment'
},
{
userId: 1,
likeableId: 1,
likeableType: 'comment'
},
], {});
```
The important thing to note from this is that each object in your array must have a `userId`, `likeableId`, and `likeableType`. The `userId` must exist as an `id` on the Users table, and the `likeableType` must be either `'post'` or `'comment'`, since that's how we set up our enum.
>Note: Your seeder will **not** throw an error if the `likeableId` does not exist on the Posts/Comments tables. That is because there is no explicit link to those tables, merely a custom link you set up through associations. Be very careful with your seeding!
Finally, no two objects can have the exact same values, as you set up that composite unique key in your migration.
Once you have all the seeding done for your different tables, you can set a sample route to make sure everything is set up correctly. Here is an example:
```javascript=
router.get(
'/:id',
asyncHandler(async (req, res) => {
const id = req.params.id;
const user = await User.findByPk(id, {
include: [
{
model: Post,
as: 'likedPosts'
},
{
model: Comment,
as: 'likedComments'
},
]
})
const post = await Post.findByPk(id, {
include:
{
model: User,
as: 'likingUsers'
}
})
const comment = await Comment.findByPk(id, {
include:
{
model: User,
as: 'likingUsers'
}
})
return res.json({
user,
post,
comment,
});
})
);
```
This is just a simple route meant to take in an integer that can be used to query for a few different models as the id. Each query eager loads the appropriate associated models. You can use something similar to compare to your seed data and ensure that the associations are all set up correctly.
And with that, you're done! Congratulations on setting up a Many-to-Many polymorphic association in Sequelize!
# Epilogue
---
Congratulations! You've made it through at least one section of this guide on polymorphic associations. You are now ready to start cleaning up some database designs to allow for this more dynamic setup to associations. This is by no means a fully comprehensive guide to polymorphic associations in Sequelize. As I noted before, I changed some of the standard code from the Sequelize documentation to better fit my needs (the "afterFind" hook). I encourage you to test stuff out, find the limits of the ORM, and generally learn how to make Sequelize work best for you!
I hope this guide was helpful to you. If you’d like to see the Flask style of doing these very same setups, that walkthrough is [here](https://hackmd.io/@chrisoney/rkNWNTt_d). I’m also working on a guide for polymorphic associations in a Ruby on Rails project, though the timeline for that is iffy.
If you have any feedback or questions, please do not hesitate to reach out to me over Slack. Have a wonderful day and best of luck on whichever project you’re working on!
###### tags: `Sequelize` `Polymorphic Associations`