Understanding Sequelize Associations: Part 3: Many to Many (n:m) mapping
This is the last part of 3 part blog series in which I explain sequelize associations by creating a CRUD app. The description and links of each part is as follows:
- Part 1: One-to-One (1:1) Mapping
- Part 2: One-to-Many (1:n) Mapping
- Part 3: Many-to-Many (n:m) Mapping
This blog assumes that you have basic understanding of NodeJs, Databases and Sequelize.
Source repo for this blog
1. What are we going to do in this blog
In this blog, I will explain n:m mapping in sequelize by creating a CRUD app. We will be working on the same app we used in the last blog. The tech stack of the app is as follows:
- Backend: HapiJS
- Database: PostgreSQL
- ORM: Sequelize (Obviously!)
Till the last blog, we have created tables: users
, userDetails
and posts
. In this part, we will create an additional table called groups
. This table will have n:m (many to many) relation with our users
table. We will associate these two tables through users_groups
table. Our through table will contain the mapping of userId
and groupId
, which belongs to the users
and groups
table respectively. The updated ER diagram is as follows:
2. Generate Models
We have already created users
, usersDetails
and posts
in the last post. Now, we will create groups
model. We will also have to create a through table named users_groups
. Similar to the last time, we will first create our models using sequelize-cli, then modify the generated models according to our associations
2.1 Create models using sequelize-cli
Run the following command to create our new model groups
. This command will also generate relevant migrations and models.
node_modules/.bin/sequelize model:generate --name groups --attributes groupName:string
The following will generate users_groups
migration and relevant model:
node_modules/.bin/sequelize model:generate --name users_groups --attributes userId:integer,groupId:integer
2.2 Modify models for association
Now that we have our migrations and models generated by sequelize, we will modify them according to our associations. Find the migration titled like: create-users-groups
and modify the userId
, and groupId
attribute to match as follows:
userId: {
type: Sequelize.INTEGER,
references: { model: 'users', key: 'id' },
onDelete: 'CASCADE',
},
groupId: {
type: Sequelize.INTEGER,
references: { model: 'groups', key: 'id' },
onDelete: 'CASCADE',
}
Now, we will start modifying models. Find the model groups
and add associations as follows:
groups.associate = (models) => {
groups.belongsToMany(models.users, {
foreignKey: 'groupId',
through: 'users_groups',
as: 'users'
});
};
Next, we will modify users
model. We already have some associations defined on this model. We will add one more association in it. The final associations should look as follows:
users.associate = function (models) {
users.hasOne(models.userDetails, {
foreignKey: 'userId',
as: 'userDetails',
onDelete: 'CASCADE'
});
users.hasMany(models.posts, {
foreignKey: 'userId',
as: 'posts'
});// we are adding the following
users.belongsToMany(models.groups, {
foreignKey: 'userId',
through: 'users_groups',
as: 'groups'
});
};
Notice that the above two models have belongsToMany associations, and they have through table defined. This is to tell sequelize the model that it should map while creating our many-to-many relation. Another crucial thing to notice here is the value of foreignKey
field. Until now, we have been using the field of the source model in the definition. But things are a little different with belongsToMany
keyword. Now, we are using the foreign key in our target model i.e. our through table. This stack overflow answer summarises the usage quite well
Next, we will modify users_groups
model, which is our through
table. Add the following associations:
users_groups.associate = (models) => {
users_groups.belongsTo(models.users, {
foreignKey: 'userId',
as: 'user'
});
users_groups.belongsTo(models.groups, {
foreignKey: 'groupId',
as: 'group'
});
};
2.3 Side note: what are hasOne, belongsTo, hasMany, belongsToMany ?
I explained this in previous parts too, but in order to keep the blog complete, I’m again explaining them
All the four keywords are used to define associations in our sequelize model. One way to differentiate them is to consider the following points:
1. Whenever the foreign key is defined on the source model (the model for which we are writing this association), use belongsTo or belongsToMany
2. If the foreign key is defined on the target model (the model with which we will be associating), use hasOne or hasMany
3. If we are considering multiple mappings (1:n, or n:m), then, we have to use hasMany or belongsToMany
For eg, in this scenario, userDetails
has the foreign key userId
. Hence, I have used hasOne
constraint. Read like: model users
has one key defined on the model userDetails
with name key as userId
.
Similarly, if we had defined a belongsTo relation on userDetails
, this would have been as follows:
userDetails.associate = function (models) {
userDetails.belongsTo(models.users, {
foreignKey: 'userId',
as: 'users'
});
};
This can be read like: model userDetails
has a foreign key which belongs to the model users
. The foreign key is userId
.
2.4 Run migrations
Now that we have all our models and migrations ready. We will run the migrations, so that our corresponding tables are replicated in our database. Run the following command:
node_modules/.bin/sequelize db:migrate
Sequelize will run new migrations only. We will now be able to see foreign key constraints defined on our tables:
3. Creating CRUD app
We will now move on by modifying our CRUD app that we built in the last post. We will add new API for working with groups. Also, we will have to modify our existing users
API, to reflect new changes. The complete file can be found here: Users API, Groups API
3.1 Create API
We need to have two create API. One to create a group, another to add user to a particular group. The code for create group API looks as follows:
The code for adding user to a group:
We also need to modify our users API. We now need to pass groups details along with other details. A sample payload for this API will now be as follows:
{
"name": "ABCD",
"userName": "abcd.abcd",
"mobileNum": "1234567890",
"address": "Earth",
"posts": [{
"title": "some post title 1"
}, {
"title": "some post title 2"
}, {
"title": "some post title 3"
}, {
"title": "some post title 4"
}],
"groups": {
"groupName": "group with users"
}
}
The corresponding changes in our create users API is as follows:
In case you want to see the full file, find it here
3.2 Read API
We will have two read API for groups. One will fetch us all the groups. Another will fetch us a particular group with all its users.
Following is code for first API which will fetch all groups:
Following is the code for second API, which fetches users of a particular group:
3.3 Update API
API for updating posts can be written as shown below:
3.4 Delete API
Similar to read API, we will have two delete API. One will delete a particular group. Another will delete a user from a group
The code for these API is as follows:
Wrapping Up
In this blog, I showed how you can create n:m mapping and utilise sequelize functions to do basic tasks. This blog marks the end of my series of sequelize associations. I hope you liked it. If you have any questions, please comment them. I will try to answer them as soon as possible
Source repo for this blog: link
Complete repo: