Junction Model Pattern: Many-to-Many - Sequelize

Many-to-many is a common modeling relationship between two entities. Here's one way to handle it with the Sequelize ORM.

Let's assume you're building a blog with Sequelize.

On your blog, you can create a bunch of Posts. As a way to describe your post, it can belong to many different Genres.

This blog post for example, might have the Sequelize, Orm, and Web Development tag.

So far, we have two models: Post and Genre.

// Post.js
module.exports = function(sequelize, DataTypes) {
  const Post =  sequelize.define('post', {
    post_id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      autoIncrement: true,
      primaryKey: true
    },
    text: {
      type: DataTypes.TEXT,
      allowNull: false
    }
  }, {
    timestamps: true,
    underscored: true,
    tableName: 'post',
  });

  return Post;
};
// Genre.js
module.exports = function(sequelize, DataTypes) {
  const Genre =  sequelize.define('genre', {
    genre_id: {
      type: DataTypes.INTEGER(11),
      allowNull: false,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: DataTypes.STRING(60),
      allowNull: false,
      unique: true
    }
  }, {
    timestamps: true,
    underscored: true,
    tableName: 'genre',
  });

  return Genre;
};

Great. Now how do we model the many-to-many relationship between them?

Modeling the junction table

We need to create a junction table to store the relationship between the two.

In order to model it, we'll end up with three tables instead of just two.

I know that there's a way to get Sequelize to create this for you simply by using the correct association methods, but I like to be explicit about it.

Let's go ahead and create the additional model now.

// tagPostGenre.js
module.exports = function(sequelize, DataTypes) {
  const TagPostGenre = sequelize.define('tag_post_genre', {
    tag_post_genre_id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.INTEGER(11),
      primaryKey: true
    },
    post_id: {
      type: DataTypes.INTEGER(11),
      primaryKey: false,
      references: {
        model: 'post',
        key: 'post_id'
      },
      onDelete: 'cascade',
      onUpdate: 'cascade',
      unique: 'unique-genre-per-post'
    },
    genre_id: {
      type: DataTypes.INTEGER(11),
      primaryKey: false,
      references: {
        model: 'genre',
        key: 'genre_id'
      },
      onDelete: 'cascade',
      onUpdate: 'cascade',
      unique: 'unique-genre-per-post'
    },
  }, {
    timestamps: true,
    underscored: true,
    tableName: 'tag_post_genre'
  });

  return TagPostGenre;
};

Also, it's not necessary to prepend the model name with "Tag-", I just like to do that because it allows me to understand at a glance that that table is a Junction table between the other two tables mentioned in the name.

Here's what's important about this so far:

  • We've created foreign key relationships to the Post and Genre model.
  • We added a composite unique constraint in order to prevent a post from adding the same genre twice.

Now that we have the model created, we have to associate them all together with Sequelize so that we can get the benefits of using the convenience methods they expose.

Update the Associations

Add the associations on TagPostGenre junction table/model.

TagPostGenre.associate = (models) => {
  TagPostGenre.belongsTo(models.Post, { foreignKey: 'post_id', targetKey: 'post_id', as: 'Post' });
  TagPostGenre.belongsTo(models.Genre, { foreignKey: 'genre_id', targetKey: 'genre_id', as: 'Genre' });
}

Add the association on Genre.

Genre.associate = (models) => {
  Genre.belongsToMany(models.Post, { as: 'PostsInGenre', through: models.TagPostGenre, foreignKey: 'genre_id'});
}

Finally, add the association on Post.

Post.associate = (models) => {
  Post.belongsToMany(models.Genre, { as: 'GenresForPost', through: models.TagPostGenre, foreignKey: 'post_id'});
}

The as key in the second argument's config object using belongsToMany is a way to specify the alias when we're doing Eager Loading / Include Queries.

Usage

If you want to set the genres for a Post instance...

const post = await Post.findOne({ where: { post_id: 1 }});

Since we've added the associations, you should have access to the convenience methods that Sequelize adds.

In this particular case, expect to see something like setGenres() on the post instance.

const genreIds = [1,2,3];
await post.setGenres(genreIds);

const genres = await post.getGenres();
console.log(genres) // genre instances! [{}, {}, {}]

We can use the ids of the genres to set the genres for this post.

You can also retrieve all of the genres using get-"plural junction association name"(). In this case, getGenres().

You can always double check that the methods have been added by using the vscode debugger.



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


4 Comments

Submit
tejashri
9 months ago

I have following error . please hel




Unhandled rejection SequelizeDatabaseError: Can't create table 'aspen1.enquiryproducts' (errno: 150)


Henrique
6 months ago

Great article, man! Thanks a lot!


But there is one mistake:

Instead of

Post.associate = (models) => {
  Post.belongsToMany(models.Genre, { as: 'GenresForPost', through: models.TagPostGenre, foreignKey: 'genre_id'});
}


it should be


Post.associate = (models) => {
  Post.belongsToMany(models.Genre, { as: 'GenresForPost', through: models.TagPostGenre, foreignKey: 'post_id'});
}

^

|

|

|


Which means the foreignKey should be 'post_id' instead of 'genre_id'.

Khalil Stemmler
6 months ago

Ah, thanks for finding that! Fixed.

Gelo
5 months ago

What is this for unique: 'unique-genre-per-post'

?

Santiago
4 months ago

Great post! Thank you.

Btw, do you have any post related to debugging on vscode?


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Web Development



You may also enjoy...

A few more related articles

Fixing Sequelize Models with Migrations
Sequelize is a great ORM for NodeJS applications that are built on relational backends. Inevitably, you'll need to update your mod...
Over $85 billion spent on fixing bad code [bootcamps, junior devs, JavaScript, and software design principles]
More and more money is being spent by companies on maintaining bad JavaScript code. Here's where I think our industry needs to cha...
How to Get the Currently Playing Song using the Spotify Node.js API & TypeScript
A quick and straightforward guide to hooking into the Spotify's awesome API to show the current song that you're listening to. It ...
How to Prerender Comments | Gatsbyjs Guide
Prerendering dynamic data can have several advantages. For Gatsby blogs with high engagement, comments can positively impact SEO, ...

Want to be notified when new content comes out?

Join 5000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates