3.3.1: Sequelize One-To-Many (1-M) Relationships
Learning Objectives
Sequelize provides "special" relationship methods (aka mixins) attached to models to query data with 1-1, 1-M and M-M relationships
Understand steps to set up Sequelize's relationship methods
Understand how to use Sequelize's relationship methods
Understand how to use migrations to add foreign keys needed for Sequelize relationships
Introduction
Sequelize makes it easy to query for data with "special" relationship methods (aka mixins) built into Sequelize. To set up these methods we will need to tell Sequelize which models are associated with each other and how they are associated, e.g. users and posts are related via a 1-M relationship. We will also need to ensure our database tables have relevant foreign key columns using migrations if necessary. We will explore what Sequelize relationship methods look like and how to set them up in this submodule.
Recall our Users and Posts example from the SQL 1-M Relationships submodule where users have many posts, and each post belongs to a single user. In that module we demonstrated how to query posts for a given user using SQL, like the following.
SELECT * from Posts where userId=2;The equivalent query in Sequelize would look like the following. Note that all Sequelize query methods return JavaScript promises because the methods query a remote database. We can choose to handle these promises with await or .then syntax.
const posts = await Post.findAll({
where: {
userId: 2
},
});We will use Sequelize instead of raw SQL in our apps because writing queries in JavaScript instead of SQL strings allows VS Code to more easily catch syntax errors for us. This makes our apps more robust and less verbose.
Sequelize Associations
Let's go through Sequelize's official introduction to associations section by section.
Defining the Sequelize associations
Notice the 4 types of associations Sequelize uses to specify relationships between models:
HasOne,BelongsTo,HasManyandBelongsToMany. We will useBelongsToandHasManyfor 1-M relationships.HasOneandBelongsToare used for 1-1 relationships andBelongsToManyis used for M-M relationships.To tell Sequelize that model
Ahas a 1-M relationship with modelBandAis the "1" andBis the "M" in the relationship, we would callA.hasMany(B);andB.belongsTo(A);. Both calls are needed to include Sequelize relationship methods on bothAandB. SinceBis the "M" in the relationship, Sequelize will assume there is a foreign keyAId(by default the related model's name (i.e.A) followed byId) that referencesA's primary keyidinB's model and underlying SQL table.No need to worry about the options 2nd parameter,
hasOneandbelongsToManymethods for nowNote in which table Sequelize expects the foreign key to be for each association. When
A.hasMany(B), foreign key is in the target modelB. WhenB.belongsTo(A), foreign key is in the source modelB.
Creating the standard relationships
We will focus on One-To-Many relationships in this submodule with
hasManyandbelongsToassociations
One-To-Many relationships (1-M)
Note the explanation that there is only 1 option for which table contains the foreign key in a 1-M relationship: the "M" table.
In our apps, we will declare Sequelize associations like
Team.hasMany(Player);andPlayer.belongsTo(Team);in class definitions indb/models/team.jsanddb/models/player.js, the files in which we define our models. We will declare the associations from within each class using thethiskeyword, likethis.hasMany(models.Player)from theTeamclass andthis.belongsTo(models.Team)from thePlayerclass. More examples in "Update models and migrations" section below.We will not use
syncas explained in Rocket's previous Sequelize submodule because it can cause unintended behaviour in production.No need to worry about the Options section for now, we will stick to the defaults first
Basics of queries involving associations
Sequelize uses a 1-1 relationship between
ShipandCaptainmodels here but the concepts are the same for 1-M relationshipsLazy loading is fetching data without using joins. Eager loading is fetching data with joins using Sequelize syntax.
getShip()is one of the "special" relationship methods we mentioned at the top of this page that allow us to query related data using Sequelize. In this case, becauseShipandCaptainare related with a 1-1 relationship, we can callgetShip()on an instance ofCaptainto get that captain's ship. More on these methods in the Special methods/mixins section below.Eager loading can be helpful to retrieve associated data in a single database query. We will use this in our Bigfoot SQL M-M exercise.
Ignore the
save()method, we will not use it because we will use the built-increate,updateanddestroymethods that performsaveautomatically.
Special methods/mixins added to instances
This section documents all "special" relationship methods we referred to at the top of this page. Note that these relationship methods will only be available on model instances when we associate relevant models with the relevant association methods, e.g.
belongsToandhasMany. For example, if I declaredA.hasMany(B)andB.belongsTo(A), I could calla.getBs()(whereais an instance of modelA) but notb.getAs()(wherebis an instance of modelB) because eachBbelongs to only 1A, not multiple.All Sequelize relationship methods return JavaScript promises because they query a remote database
In case you're wondering, "foo" and "bar" (or just "foobar") are common placeholder names in computer science
Note Sequelize performs pluralisation automatically and intelligently such that we can assume the relationship method names use accurate English, e.g.
getPersonfor a 1-1 relationship andgetPeoplefor a 1-M relationship with aPersonmodel.We can ignore the special methods for
belongsToManyassociations for now. We will revisit them in the Sequelize M-M submodule.Great work! This is the most important section of this submodule, and we will be using these "special" relationship methods often!
Why associations are defined in pairs?
Always define relationships in pairs in Sequelize, e.g.
hasManyandbelongsTofor 1-M relationships! This will ensure we have the relevant built-in relationship methods when we need them.Ignore the sections below this one in the Sequelize Associations page. We will not use them for now, if at all at Rocket. We will solidify our fundamentals first before learning advanced concepts.
Update models and migrations to add foreign keys
Introduction
The above documentation showed us how to declare Sequelize associations and use Sequelize relationship methods, but touched little on how to add foreign keys in model definitions and in our database schema using migrations. For Sequelize associations to work we will also need to update model and migration files such that our models and our database have the relevant foreign keys.
Let's again use our hypothetical Users and Posts social media example where each user has many posts. Because there is a 1-M relationship between Users and Posts respectively, there needs to be a foreign key UserId on the Posts table. Let's see how our models and migrations might look with this foreign key.
Models
Most of db/models/user.js where we define our User model is boilerplate except for this.hasMany(models.post) where we declare User's association with the Post model
"use strict";
const { Model } = require("sequelize");
module.exports = (sequelize, DataTypes) => {
class User extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
this.hasMany(models.post);
}
}
User.init(
{
name: DataTypes.STRING,
},
{
sequelize,
modelName: "user",
underscored: true,
}
);
return User;
};Like User, we also declare an association in Post, this time belongsTo instead of hasMany. Unlike User, Post needs a foreign key to complete their 1-M relationship, and we will give this foreign key a non-default name for clarity. We define this foreign key as AuthorId instead of UserId, because UserId is less precise and could cause confusion as more users are associated with posts, e.g. users that like or comment on posts.
There are 2 points to note:
In our association method
belongsTowe add a 2nd options parameter{ as: "Author" })that instructs Sequelize to use theAuthoralias forUsers in this relationship. This has 2 consequences:All Sequelize relationship methods that would otherwise have looked like
post.getUser()will now look likepost.getAuthor()instead (official explanation)Sequelize now expects there to be a foreign key
AuthorIdon thePostmodel andPoststable instead ofUserId
We define the foreign key
AuthorIdas a property of thePostmodel. In addition to specifying the property's data type, we also specify the model and property this foreign key references, in this case theidproperty of theUsermodel.
"use strict";
const { Model } = require("sequelize");
module.exports = (sequelize, DataTypes) => {
class Post extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
this.belongsTo(models.user, { as: "author" });
}
}
Post.init(
{
date: DataTypes.DATE,
content: DataTypes.TEXT,
AuthorId: {
type: DataTypes.INTEGER,
references: {
model: "user",
key: "id",
},
},
},
{
sequelize,
modelName: "post",
underscored: true,
}
);
return Post;
};The above changes will allow us to use Sequelize relationships in our apps, assuming our underlying SQL database also has the relevant foreign keys. We will need to add new database migrations to add relevant foreign keys to our database schema.
Migrations
The following database migrations assume we do not have existing tables in our database. If we do have existing tables in our database whose schemas need to be edited, we will either need to:
Edit existing migrations, drop existing database (
dropdbis a convenient CLI method), create new database (createdbis a convenient CLI method) and re-run migrations (npx sequelize db:migrate)Create new migrations to edit tables in existing database (
npx sequelize migration:generate)
Rocket recommends option #1 for apps with no live user data to maximise development speed. Apps with live user data only have option #2, since we should never drop a database with live user data.
The migration to create the users table has no foreign key, since there is no foreign key on the User model.
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("users", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("users");
},
};The migration to create the posts table contains the author_id foreign key, declared almost identically as it was in db/models/post.js above. Sequelize expects the value for the models key in the references object to reference a plural table name. Documentation for declaring foreign keys in migrations here.
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable("posts", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
date: {
type: Sequelize.DATE,
},
content: {
type: Sequelize.TEXT,
},
author_id: {
type: Sequelize.INTEGER,
references: {
model: "users",
key: "id",
},
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable("posts");
},
};Once we have updated our models to include associations and foreign keys, updated our migrations to include foreign keys and run those migrations on our database, we are ready to start using Sequelize relationship methods in our apps!