How to make join queries using Sequelize on Node.js

Learn how to make join queries using sequelize on node.js with practical examples, diagrams, and best practices. Covers mysql, node.js, orm development techniques with visual explanations.

Mastering Join Queries with Sequelize in Node.js

Hero image for How to make join queries using Sequelize on Node.js

Learn how to perform various types of join queries (one-to-one, one-to-many, many-to-many) using Sequelize ORM in your Node.js applications, enhancing data retrieval efficiency.

Sequelize is a powerful Object-Relational Mapper (ORM) for Node.js that simplifies interaction with relational databases like MySQL, PostgreSQL, SQLite, and MSSQL. One of the most common and crucial database operations is joining related tables to retrieve comprehensive datasets. This article will guide you through setting up associations and executing different types of join queries using Sequelize, ensuring you can efficiently fetch interconnected data in your Node.js applications.

Understanding Sequelize Associations

Before diving into join queries, it's essential to understand how Sequelize defines relationships between models. These associations dictate how your tables are linked and how Sequelize constructs the underlying SQL JOIN statements. The primary association types are hasOne, belongsTo, hasMany, and belongsToMany.

erDiagram
    CUSTOMER ||--o{ ORDER : places
    ORDER ||--|{ LINE_ITEM : contains
    PRODUCT ||--o{ LINE_ITEM : includes
    CUSTOMER { 
        int id PK
        string name
        string email
    }
    ORDER { 
        int id PK
        int customerId FK
        datetime orderDate
    }
    LINE_ITEM { 
        int id PK
        int orderId FK
        int productId FK
        int quantity
        decimal price
    }
    PRODUCT { 
        int id PK
        string name
        decimal price
    }

Entity-Relationship Diagram illustrating common database associations

Let's define some example models: User, Post, and Tag. A User can have many Posts, a Post belongs to a User, and a Post can have many Tags (and a Tag can belong to many Posts).

// models/user.js
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    username: DataTypes.STRING,
    email: DataTypes.STRING
  });
  User.associate = (models) => {
    User.hasMany(models.Post, { foreignKey: 'userId', as: 'posts' });
  };
  return User;
};

// models/post.js
module.exports = (sequelize, DataTypes) => {
  const Post = sequelize.define('Post', {
    title: DataTypes.STRING,
    content: DataTypes.TEXT
  });
  Post.associate = (models) => {
    Post.belongsTo(models.User, { foreignKey: 'userId', as: 'author' });
    Post.belongsToMany(models.Tag, { through: 'PostTags', foreignKey: 'postId', as: 'tags' });
  };
  return Post;
};

// models/tag.js
module.exports = (sequelize, DataTypes) => {
  const Tag = sequelize.define('Tag', {
    name: DataTypes.STRING
  });
  Tag.associate = (models) => {
    Tag.belongsToMany(models.Post, { through: 'PostTags', foreignKey: 'tagId', as: 'posts' });
  };
  return Tag;
};

// index.js (for association setup)
const Sequelize = require('sequelize');
const config = require('../config/config.json');
const db = {};

const sequelize = new Sequelize(config.development);

db.sequelize = sequelize;
db.Sequelize = Sequelize;

db.User = require('./user')(sequelize, Sequelize.DataTypes);
db.Post = require('./post')(sequelize, Sequelize.DataTypes);
db.Tag = require('./tag')(sequelize, Sequelize.DataTypes);

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

module.exports = db;

Defining Sequelize models and their associations

Executing Join Queries with include

Sequelize uses the include option within findAll, findOne, and other query methods to perform join operations. This option allows you to specify which associated models you want to load along with the primary model.

1. One-to-Many Join (User with their Posts)

To fetch a user and all their associated posts, you'll use the hasMany association. Sequelize will perform an INNER JOIN by default.

const db = require('./models'); // Assuming your models are in a 'models' directory

async function getUserWithPosts() {
  try {
    const user = await db.User.findOne({
      where: { username: 'john_doe' },
      include: [{
        model: db.Post,
        as: 'posts' // Use the alias defined in the association
      }]
    });

    if (user) {
      console.log('User:', user.toJSON());
      console.log('Posts:', user.posts.map(post => post.toJSON()));
    } else {
      console.log('User not found.');
    }
  } catch (error) {
    console.error('Error fetching user with posts:', error);
  }
}

getUserWithPosts();

Fetching a user and their associated posts

2. Many-to-One Join (Post with its Author)

To fetch a post and its author, you'll use the belongsTo association. This is essentially the reverse of the one-to-many join from the perspective of the Post model.

const db = require('./models');

async function getPostWithAuthor() {
  try {
    const post = await db.Post.findOne({
      where: { title: 'My First Blog Post' },
      include: [{
        model: db.User,
        as: 'author' // Use the alias defined in the association
      }]
    });

    if (post) {
      console.log('Post:', post.toJSON());
      console.log('Author:', post.author.toJSON());
    } else {
      console.log('Post not found.');
    }
  } catch (error) {
    console.error('Error fetching post with author:', error);
  }
}

getPostWithAuthor();

Fetching a post and its associated author

3. Many-to-Many Join (Post with its Tags)

For many-to-many relationships, Sequelize uses a through table (e.g., PostTags). When you include a many-to-many associated model, Sequelize automatically handles the join through this intermediary table.

const db = require('./models');

async function getPostWithTags() {
  try {
    const post = await db.Post.findOne({
      where: { title: 'My First Blog Post' },
      include: [{
        model: db.Tag,
        as: 'tags' // Use the alias defined in the association
      }]
    });

    if (post) {
      console.log('Post:', post.toJSON());
      console.log('Tags:', post.tags.map(tag => tag.toJSON()));
    } else {
      console.log('Post not found.');
    }
  } catch (error) {
    console.error('Error fetching post with tags:', error);
  }
}

getPostWithTags();

Fetching a post and its associated tags

Advanced Join Options

Sequelize provides several options to fine-tune your join queries, including specifying the join type, selecting specific attributes, and applying conditions to included models.

const db = require('./models');

async function getPostsWithOptionalAuthorAndSpecificAttributes() {
  try {
    const posts = await db.Post.findAll({
      attributes: ['id', 'title'], // Select only specific attributes from Post
      include: [{
        model: db.User,
        as: 'author',
        attributes: ['username', 'email'], // Select specific attributes from User
        required: false, // LEFT OUTER JOIN: include posts even if no author
        where: { username: 'john_doe' } // Condition on the included model
      }, {
        model: db.Tag,
        as: 'tags',
        attributes: ['name'],
        through: { attributes: [] } // Exclude attributes from the join table (PostTags)
      }]
    });

    console.log('Posts with optional author and tags:', posts.map(post => post.toJSON()));
  } catch (error) {
    console.error('Error fetching posts with advanced options:', error);
  }
}

getPostsWithOptionalAuthorAndSpecificAttributes();

Using required: false for LEFT JOIN, selecting specific attributes, and applying where clauses on included models

You can also nest include options to perform multi-level joins, for example, fetching users, their posts, and the tags associated with each post.

const db = require('./models');

async function getUserPostsAndTags() {
  try {
    const user = await db.User.findOne({
      where: { username: 'john_doe' },
      include: [{
        model: db.Post,
        as: 'posts',
        include: [{
          model: db.Tag,
          as: 'tags',
          attributes: ['name'],
          through: { attributes: [] }
        }]
      }]
    });

    if (user) {
      console.log('User with posts and tags:', JSON.stringify(user, null, 2));
    } else {
      console.log('User not found.');
    }
  } catch (error) {
    console.error('Error fetching user, posts, and tags:', error);
  }
}

getUserPostsAndTags();

Performing nested joins to fetch users, their posts, and the tags for each post