How to make join queries using Sequelize on Node.js
Mastering Join Queries with Sequelize in 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
as
aliases in your associations. This makes your queries more readable and helps Sequelize differentiate between multiple associations to the same model.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.
include
performs an INNER JOIN
. If you want to include records from the primary model even if there are no matching associated records, you need to specify required: false
for an LEFT OUTER JOIN
.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
include
statements. Each include
translates to a JOIN operation in SQL, which can become slow on large datasets. Consider using separate queries or Sequelize's scope
feature for complex scenarios.