How to Use Sequelize belongsToMany Associations: Implementing User-Project Many-to-Many Relationships and Including Associated Models

In modern web development, managing relationships between database tables is a common challenge. One of the most versatile relationship types is the many-to-many relationship, where multiple instances of one model can associate with multiple instances of another. For example:

  • A User can be part of multiple Projects.
  • A Project can have multiple Users.

Sequelize, a popular Node.js ORM (Object-Relational Mapper), simplifies implementing many-to-many relationships with its belongsToMany association. This guide will walk you through setting up a User-Project many-to-many relationship, defining associations, querying with associated models, and handling advanced scenarios like custom join tables with extra attributes.

Table of Contents#

  1. Prerequisites
  2. Project Setup
  3. Defining Models: User, Project, and the Join Table
  4. Setting Up belongsToMany Associations
  5. Syncing the Database
  6. CRUD Operations with Associations
  7. Including Associated Models in Queries
  8. Advanced: Custom Join Tables with Additional Attributes
  9. Troubleshooting Common Issues
  10. Conclusion
  11. References

Prerequisites#

Before diving in, ensure you have the following:

  • Node.js (v14+ recommended) and npm installed.
  • Basic knowledge of Node.js, Express (optional, for API examples), and SQL.
  • A database (PostgreSQL, MySQL, SQLite, or MSSQL; we’ll use PostgreSQL for this guide).
  • Sequelize CLI (optional but helpful for database management).

Install required packages:

npm install sequelize pg pg-hstore  # pg/pg-hstore for PostgreSQL; use mysql2 for MySQL, etc.  
npm install --save-dev sequelize-cli  # Optional, for CLI tools  

Project Setup#

Let’s initialize a Sequelize project. If using the Sequelize CLI:

npx sequelize-cli init  # Creates config, models, migrations, seeders folders  

This generates a config/config.json file. Update it with your database credentials:

// config/config.json  
{  
  "development": {  
    "username": "your_db_user",  
    "password": "your_db_password",  
    "database": "user_project_db",  
    "host": "localhost",  
    "dialect": "postgres"  
  }  
}  

If not using the CLI, create a sequelize.js file to initialize the Sequelize instance:

// sequelize.js  
const { Sequelize } = require('sequelize');  
 
const sequelize = new Sequelize('user_project_db', 'your_db_user', 'your_db_password', {  
  host: 'localhost',  
  dialect: 'postgres'  
});  
 
module.exports = sequelize;  

Defining Models: User, Project, and the Join Table#

In many-to-many relationships, Sequelize uses a join table (also called a junction table) to link the two models. This table stores foreign keys referencing the primary keys of the associated models.

Step 1: Define the User Model#

Create models/user.js:

// models/user.js  
const { DataTypes } = require('sequelize');  
const sequelize = require('../sequelize');  
 
const User = sequelize.define('User', {  
  name: {  
    type: DataTypes.STRING,  
    allowNull: false  
  },  
  email: {  
    type: DataTypes.STRING,  
    allowNull: false,  
    unique: true,  
    validate: {  
      isEmail: true  
    }  
  }  
});  
 
module.exports = User;  

Step 2: Define the Project Model#

Create models/project.js:

// models/project.js  
const { DataTypes } = require('sequelize');  
const sequelize = require('../sequelize');  
 
const Project = sequelize.define('Project', {  
  title: {  
    type: DataTypes.STRING,  
    allowNull: false  
  },  
  description: {  
    type: DataTypes.TEXT  
  }  
});  
 
module.exports = Project;  

Step 3: The Join Table#

By default, Sequelize auto-generates a join table when using belongsToMany. The table name is typically the plural of the model names in alphabetical order (e.g., UserProject for User and Project).

For simple cases (only foreign keys), you don’t need to define the join table explicitly. Sequelize handles it. For advanced cases (e.g., adding role or startDate to the join table), we’ll define it manually later.

Setting Up belongsToMany Associations#

To link User and Project with a many-to-many relationship, use belongsToMany on both models. This tells Sequelize to create the join table and manage associations.

Update the models to include associations:

User Model (Add Association)#

// models/user.js  
const Project = require('./project');  
 
User.belongsToMany(Project, {  
  through: 'UserProject',  // Name of the join table  
  foreignKey: 'userId',    // Foreign key in the join table referencing User  
  as: 'projects'           // Alias for the association (optional but recommended)  
});  
 
module.exports = User;  

Project Model (Add Association)#

// models/project.js  
const User = require('./user');  
 
Project.belongsToMany(User, {  
  through: 'UserProject',  // Must match the join table name from User  
  foreignKey: 'projectId', // Foreign key in the join table referencing Project  
  as: 'users'              // Alias for the association  
});  
 
module.exports = Project;  

Key Parameters Explained:#

  • through: The name of the join table (required for belongsToMany).
  • foreignKey: The column name in the join table that references the current model (e.g., userId in UserProject links to User.id).
  • as: An alias for the association, making queries more readable (e.g., user.getProjects() instead of user.getProject()).

Syncing the Database#

Now, sync the models with the database to create tables. Run this in a script (e.g., sync.js):

// sync.js  
const sequelize = require('./sequelize');  
const User = require('./models/user');  
const Project = require('./models/project');  
 
async function syncDB() {  
  try {  
    await sequelize.sync({ force: true }); // force: true drops existing tables (use in dev only!)  
    console.log('Database synced successfully');  
  } catch (error) {  
    console.error('Sync failed:', error);  
  }  
}  
 
syncDB();  

Run the script:

node sync.js  

Sequelize will create three tables:

  • Users (from User model).
  • Projects (from Project model).
  • UserProject (join table with userId and projectId).

CRUD Operations with Associations#

Let’s explore common operations: creating, reading, updating, and deleting associations.

1. Creating a User and Associating a Project#

Create a user, a project, then link them using addProject (auto-generated by Sequelize):

const { User, Project } = require('./models');  
 
async function createUserWithProject() {  
  try {  
    // Create a user  
    const user = await User.create({  
      name: 'John Doe',  
      email: '[email protected]'  
    });  
 
    // Create a project  
    const project = await Project.create({  
      title: 'Sequelize Tutorial',  
      description: 'Learn many-to-many associations'  
    });  
 
    // Associate the user with the project  
    await user.addProject(project);  
 
    console.log('User and project associated!');  
  } catch (error) {  
    console.error('Error:', error);  
  }  
}  
 
createUserWithProject();  

2. Creating a User with Projects (Bulk Association)#

Use create with include to create a user and associate projects in one step:

async function createUserWithProjects() {  
  const user = await User.create({  
    name: 'Jane Smith',  
    email: '[email protected]',  
    projects: [  
      { title: 'Website Redesign', description: 'Update UI/UX' },  
      { title: 'API Development', description: 'Build RESTful endpoints' }  
    ]  
  }, {  
    include: [{ model: Project, as: 'projects' }] // Include the projects association  
  });  
 
  console.log('User with projects:', user.toJSON());  
}  
 
createUserWithProjects();  

Sequelize creates the user, the two projects, and links them in UserProject.

3. Adding/Removing Associations#

Use addProject, addProjects, removeProject, or removeProjects to modify associations:

async function updateUserProjects() {  
  const user = await User.findOne({ where: { name: 'John Doe' } });  
  const newProject = await Project.create({ title: 'Mobile App', description: 'React Native' });  
 
  // Add a single project  
  await user.addProject(newProject);  
 
  // Add multiple projects (array)  
  const projects = await Project.findAll({ where: { title: ['Website Redesign', 'API Development'] } });  
  await user.addProjects(projects);  
 
  // Remove a project  
  await user.removeProject(newProject);  
}  

Including Associated Models in Queries#

To fetch a user and their projects (or vice versa), use include in find methods (eager loading).

1. Get a User with Their Projects#

async function getUserWithProjects() {  
  const user = await User.findByPk(1, {  
    include: [{  
      model: Project,  
      as: 'projects',  // Must match the alias defined in the association  
      attributes: ['id', 'title']  // Optional: specify which project attributes to include  
    }]  
  });  
 
  console.log('User with projects:', user.toJSON());  
}  
 
getUserWithProjects();  

Output:

{  
  "id": 1,  
  "name": "John Doe",  
  "email": "[email protected]",  
  "projects": [  
    { "id": 1, "title": "Sequelize Tutorial" },  
    { "id": 2, "title": "Website Redesign" }  
  ]  
}  

2. Get a Project with Its Users#

async function getProjectWithUsers() {  
  const project = await Project.findByPk(1, {  
    include: [{  
      model: User,  
      as: 'users',  
      attributes: ['id', 'name'], // Exclude sensitive data like email  
      through: { attributes: [] } // Exclude join table data (optional)  
    }]  
  });  
 
  console.log('Project with users:', project.toJSON());  
}  

3. Filter and Order Included Models#

Add where or order to the include clause to filter associated data:

// Get users who are part of 'Sequelize Tutorial'  
const usersInTutorial = await User.findAll({  
  include: [{  
    model: Project,  
    as: 'projects',  
    where: { title: 'Sequelize Tutorial' } // Filter projects by title  
  }]  
});  
 
// Order projects by title  
const user = await User.findByPk(1, {  
  include: [{  
    model: Project,  
    as: 'projects',  
    order: [['title', 'ASC']]  
  }]  
});  

Advanced: Custom Join Tables with Additional Attributes#

Sometimes, the join table needs more than just foreign keys (e.g., a role for a user in a project, or startDate). To add custom attributes, define the join table explicitly.

Step 1: Define the Custom Join Table (UserProject)#

Create models/userProject.js:

// models/userProject.js  
const { DataTypes } = require('sequelize');  
const sequelize = require('../sequelize');  
 
const UserProject = sequelize.define('UserProject', {  
  role: {  
    type: DataTypes.STRING,  
    allowNull: false,  
    validate: {  
      isIn: [['admin', 'developer', 'viewer']] // Restrict to valid roles  
    }  
  },  
  startDate: {  
    type: DataTypes.DATE,  
    defaultValue: DataTypes.NOW  
  }  
}, {  
  timestamps: true // Adds createdAt and updatedAt (optional)  
});  
 
module.exports = UserProject;  

Step 2: Update Associations to Use the Custom Join Table#

Modify User and Project associations to reference the UserProject model instead of a string:

// models/user.js  
const Project = require('./project');  
const UserProject = require('./userProject');  
 
User.belongsToMany(Project, {  
  through: UserProject,  // Use the model instead of a string  
  foreignKey: 'userId',  
  as: 'projects'  
});  
// models/project.js  
const User = require('./user');  
const UserProject = require('./userProject');  
 
Project.belongsToMany(User, {  
  through: UserProject,  
  foreignKey: 'projectId',  
  as: 'users'  
});  

Step 3: Create/Query with Custom Join Table Attributes#

Now, when associating a user and project, include the custom attributes:

async function createUserWithRole() {  
  const user = await User.create({ name: 'Alice', email: '[email protected]' });  
  const project = await Project.create({ title: 'DevOps', description: 'CI/CD Pipeline' });  
 
  // Associate with role  
  await user.addProject(project, { through: { role: 'admin' } });  
 
  // Get user with projects and roles  
  const userWithRoles = await User.findByPk(user.id, {  
    include: [{  
      model: Project,  
      as: 'projects',  
      through: { attributes: ['role', 'startDate'] } // Include join table attributes  
    }]  
  });  
 
  console.log(userWithRoles.toJSON());  
}  

Output includes the role and startDate:

{  
  "id": 3,  
  "name": "Alice",  
  "email": "[email protected]",  
  "projects": [  
    {  
      "id": 3,  
      "title": "DevOps",  
      "UserProject": { "role": "admin", "startDate": "2024-01-01T12:00:00.000Z" }  
    }  
  ]  
}  

Troubleshooting Common Issues#

  • "Association not found" error: Ensure as in include matches the alias in belongsToMany.
  • Foreign key mismatch: Verify foreignKey in both models points to the correct join table columns.
  • Join table not created: Run sequelize.sync({ force: true }) (dev only) to recreate tables.
  • Custom join table attributes missing: Use through: { attributes: [...] } when including associations.

Conclusion#

Sequelize’s belongsToMany association simplifies implementing many-to-many relationships. By defining associations on both models, using through to specify the join table, and leveraging include for eager loading, you can efficiently manage complex relationships. For advanced use cases, customize the join table to include additional attributes and access them in queries.

References#