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" });