3.3.1: Sequelize One-To-Many (1-M) Relationships

Learning Objectives

  1. Sequelize provides "special" relationship methods (aka mixins) attached to models to query data with 1-1, 1-M and M-M relationships

  2. Understand steps to set up Sequelize's relationship methods

  3. Understand how to use Sequelize's relationship methods

  4. 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

Official guide to Sequelize Associations
  1. Notice the 4 types of associations Sequelize uses to specify relationships between models: HasOne, BelongsTo, HasMany and BelongsToMany. We will use BelongsTo and HasMany for 1-M relationships. HasOne and BelongsTo are used for 1-1 relationships and BelongsToMany is used for M-M relationships.

  2. To tell Sequelize that model A has a 1-M relationship with model B and A is the "1" and B is the "M" in the relationship, we would call A.hasMany(B); and B.belongsTo(A);. Both calls are needed to include Sequelize relationship methods on both A and B. Since B is the "M" in the relationship, Sequelize will assume there is a foreign key AId (by default the related model's name (i.e. A) followed by Id) that references A's primary key id in B's model and underlying SQL table.

  3. No need to worry about the options 2nd parameter, hasOne and belongsToMany methods for now

  4. Note in which table Sequelize expects the foreign key to be for each association. When A.hasMany(B), foreign key is in the target model B. When B.belongsTo(A), foreign key is in the source model B.

Creating the standard relationships

Official guide to Sequelize Associations: Creating the standard relationships
  1. We will focus on One-To-Many relationships in this submodule with hasMany and belongsTo associations

One-To-Many relationships (1-M)

Official guide to Sequelize Associations: One-To-Many relationships
  1. Note the explanation that there is only 1 option for which table contains the foreign key in a 1-M relationship: the "M" table.

  2. In our apps, we will declare Sequelize associations like Team.hasMany(Player); and Player.belongsTo(Team); in class definitions in db/models/team.js and db/models/player.js, the files in which we define our models. We will declare the associations from within each class using the this keyword, like this.hasMany(models.Player) from the Team class and this.belongsTo(models.Team) from the Player class. More examples in "Update models and migrations" section below.

  3. We will not use sync as explained in Rocket's previous Sequelize submodule because it can cause unintended behaviour in production.

  4. No need to worry about the Options section for now, we will stick to the defaults first

Basics of queries involving associations

Official guide to Sequelize Associations: Basics of queries involving associations
  1. Sequelize uses a 1-1 relationship between Ship and Captain models here but the concepts are the same for 1-M relationships

  2. Lazy loading is fetching data without using joins. Eager loading is fetching data with joins using Sequelize syntax.

  3. 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, because Ship and Captain are related with a 1-1 relationship, we can call getShip() on an instance of Captain to get that captain's ship. More on these methods in the Special methods/mixins section below.

  4. 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.

  5. Ignore the save() method, we will not use it because we will use the built-in create, update and destroy methods that perform save automatically.

Special methods/mixins added to instances

Official guide to Sequelize Associations: Special methods/mixins added to instances
  1. 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. belongsTo and hasMany. For example, if I declared A.hasMany(B) and B.belongsTo(A), I could call a.getBs() (where a is an instance of model A) but not b.getAs() (where b is an instance of model B) because each B belongs to only 1 A, not multiple.

  2. All Sequelize relationship methods return JavaScript promises because they query a remote database

  3. In case you're wondering, "foo" and "bar" (or just "foobar") are common placeholder names in computer science

  4. Note Sequelize performs pluralisation automatically and intelligently such that we can assume the relationship method names use accurate English, e.g. getPerson for a 1-1 relationship and getPeople for a 1-M relationship with a Person model.

  5. We can ignore the special methods for belongsToMany associations for now. We will revisit them in the Sequelize M-M submodule.

  6. 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?

Official guide to Sequelize Associations: Why associations are defined in pairs?
  1. Always define relationships in pairs in Sequelize, e.g. hasMany and belongsTo for 1-M relationships! This will ensure we have the relevant built-in relationship methods when we need them.

  2. 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

db/models/user.js
"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:

  1. In our association method belongsTo we add a 2nd options parameter { as: "Author" }) that instructs Sequelize to use the Author alias for Users in this relationship. This has 2 consequences:

    1. All Sequelize relationship methods that would otherwise have looked like post.getUser() will now look like post.getAuthor() instead (official explanation)

    2. Sequelize now expects there to be a foreign key AuthorId on the Post model and Posts table instead of UserId

  2. We define the foreign key AuthorId as a property of the Post model. In addition to specifying the property's data type, we also specify the model and property this foreign key references, in this case the id property of the User model.

db/models/post.js
"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:

  1. Edit existing migrations, drop existing database (dropdb is a convenient CLI method), create new database (createdb is a convenient CLI method) and re-run migrations (npx sequelize db:migrate)

  2. 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.

20220531155824-create-user.js
"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.

20220531155825-create-post.js
"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!

New to Rocket Academy?

If you're not enrolled in Rocket's Bootcamp and visiting this page, check out our website to learn more about our Bootcamp course!