Sequelize Associations
Association Types
Associations are always defined in pairs. Call both sides for full functionality.
// One-to-One
User.hasOne(Profile, { foreignKey: "userId", as: "profile", onDelete: "CASCADE" });
Profile.belongsTo(User, { foreignKey: "userId", as: "user" });
// One-to-Many
User.hasMany(Post, { foreignKey: "authorId", as: "posts" });
Post.belongsTo(User, { foreignKey: "authorId", as: "author" });
// Many-to-Many (Sequelize creates join table automatically)
User.belongsToMany(Role, { through: "UserRoles", as: "roles" });
Role.belongsToMany(User, { through: "UserRoles", as: "users" });
// Many-to-Many with custom join model
User.belongsToMany(Project, { through: UserProject, as: "projects" });
Project.belongsToMany(User, { through: UserProject, as: "members" });
// Options
// foreignKey: custom FK column name
// sourceKey: source table key (default: primary key)
// as: alias for include/getter methods
// onDelete: CASCADE | SET NULL | RESTRICT (default: SET NULL)
// onUpdate: CASCADE (default)
Eager Loading (include)
// Include associated models
const users = await User.findAll({
include: [
{
model: Post,
as: "posts",
where: { published: true },
required: false, // LEFT JOIN (default: false)
attributes: ["id", "title"],
limit: 5,
order: [["createdAt", "DESC"]]
},
{
model: Profile,
as: "profile"
}
]
});
// Nested include (3 levels deep)
await User.findAll({
include: [{
model: Post,
as: "posts",
include: [{
model: Comment,
as: "comments",
include: [{ model: User, as: "commenter" }]
}]
}]
});
// Include all (not recommended in production โ use explicit includes)
await User.findAll({ include: { all: true } });
Through Tables (Custom Join Model)
// UserProject join model with extra attributes
class UserProject extends Model {
declare userId: number;
declare projectId: number;
declare role: string;
declare joinedAt: Date;
}
UserProject.init({
role: { type: DataTypes.STRING, defaultValue: "member" },
joinedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
}, { sequelize, tableName: "user_projects" });
User.belongsToMany(Project, { through: UserProject });
Project.belongsToMany(User, { through: UserProject });
// Add a user to a project with extra attributes
await project.addMember(user, { through: { role: "admin" } });
// Query with through attributes
const projects = await user.getProjects({
include: [{ model: UserProject, attributes: ["role", "joinedAt"] }]
});
// Access through model
const membership = await UserProject.findOne({
where: { userId: 1, projectId: 5 }
});
Association Methods
// hasMany / belongsToMany methods generated automatically:
// get*, set*, add*, create*, remove*, has*, count*
// hasMany: User has Posts
const posts = await user.getPosts({ where: { published: true } });
await user.createPost({ title: "Hello", body: "World" });
await user.addPost(existingPost);
await user.setPosts([post1, post2]); // replace all
await user.removePost(post);
const count = await user.countPosts();
const has = await user.hasPost(post);
// belongsToMany: User belongs to Roles
const roles = await user.getRoles();
await user.addRole(adminRole);
await user.addRoles([role1, role2]);
await user.setRoles([role1]); // replace all
await user.removeRole(guestRole);
const hasAdmin = await user.hasRole(adminRole);
Polymorphic Associations
// Polymorphic: Comment can belong to Post OR Article
Comment.init({
body: { type: DataTypes.TEXT },
commentableId: { type: DataTypes.INTEGER },
commentableType: { type: DataTypes.STRING } // "Post" or "Article"
}, { sequelize });
// Query polymorphic
const postComments = await Comment.findAll({
where: { commentableId: 5, commentableType: "Post" }
});
// Or use separate associations
Post.hasMany(Comment, { foreignKey: "commentableId",
scope: { commentableType: "Post" }, as: "comments" });
Comment.belongsTo(Post, { foreignKey: "commentableId", as: "post" });