Many_to_many
Chapter 28: Many-to-Many Relationships
Section titled “Chapter 28: Many-to-Many Relationships”Mastering Many-to-Many Entity Relationships
Section titled “Mastering Many-to-Many Entity Relationships”28.1 Many-to-Many Relationship Overview
Section titled “28.1 Many-to-Many Relationship Overview”A many-to-many relationship connects two entities where multiple instances of each entity can be associated with each other.
Many-to-Many Relationship ================================================================================
+------------------+ +------------------+ +------------------+ | User | | user_roles | | Role | +------------------+ +------------------+ +------------------+ | id (PK) | M:N | userId (FK) | M:N | id (PK) | | name | <------>| roleId (FK) |<------->| name | | email | | (composite PK) | | description | +------------------+ +------------------+ +------------------+ | | Junction Table (Join Table)
Example: - User 1 -----> Role 1, Role 2 - User 2 -----> Role 2, Role 3 - Role 1 -----> User 1, User 3 - Role 2 -----> User 1, User 2
================================================================================28.2 Basic Many-to-Many Relationship
Section titled “28.2 Basic Many-to-Many Relationship”First Entity
Section titled “First Entity”import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';import { Role } from '../roles/role.entity';
@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column({ unique: true }) email: string;
// Many users have many roles @ManyToMany(() => Role, (role) => role.users) @JoinTable() // Owning side - creates junction table roles: Role[];}Second Entity
Section titled “Second Entity”import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';import { User } from '../users/user.entity';
@Entity()export class Role { @PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) name: string;
@Column({ nullable: true }) description: string;
// Many roles belong to many users (inverse side) @ManyToMany(() => User, (user) => user.roles) users: User[];}28.3 Database Schema
Section titled “28.3 Database Schema” Generated Database Schema ================================================================================
Table: users +------------+----------+---------------+ | Column | Type | Constraints | +------------+----------+---------------+ | id | integer | PRIMARY KEY | | name | varchar | NOT NULL | | email | varchar | UNIQUE | +------------+----------+---------------+
Table: roles +------------+----------+---------------+ | Column | Type | Constraints | +------------+----------+---------------+ | id | integer | PRIMARY KEY | | name | varchar | UNIQUE | | description| varchar | | +------------+----------+---------------+
Table: user_roles_roles (Junction Table) +------------+----------+---------------+ | Column | Type | Constraints | +------------+----------+---------------+ | userId | integer | FK (users.id) | | roleId | integer | FK (roles.id) | +------------+----------+---------------+ | | | +---- FOREIGN KEY -> roles.id +------------------- FOREIGN KEY -> users.id
Primary Key: (userId, roleId)
================================================================================28.4 Owning Side vs Inverse Side
Section titled “28.4 Owning Side vs Inverse Side”Understanding the Difference
Section titled “Understanding the Difference” Owning Side vs Inverse Side ================================================================================
Owning Side (User): - Has @JoinTable() decorator - Controls the junction table - Changes here affect the database
Inverse Side (Role): - References owning side in @ManyToMany - No @JoinTable() decorator - Read-only from this side
Example: user.roles = [role1, role2]; // This will update junction table await userRepository.save(user);
role.users = [user1, user2]; // This will NOT update junction table await roleRepository.save(role); // Need to save from User side
================================================================================Custom Join Table
Section titled “Custom Join Table”@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@ManyToMany(() => Role, (role) => role.users) @JoinTable({ name: 'user_role', // Custom junction table name joinColumn: { name: 'user_id', // Custom column name for User referencedColumnName: 'id', }, inverseJoinColumn: { name: 'role_id', // Custom column name for Role referencedColumnName: 'id', }, }) roles: Role[];}28.5 Working with Many-to-Many Relationships
Section titled “28.5 Working with Many-to-Many Relationships”Adding Relations
Section titled “Adding Relations”// Method 1: Assign roles arrayconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['roles'],});
const adminRole = await roleRepository.findOne({ where: { name: 'admin' } });const editorRole = await roleRepository.findOne({ where: { name: 'editor' } });
user.roles = [adminRole, editorRole];await userRepository.save(user);
// Method 2: Add to existing rolesconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['roles'],});
const newRole = await roleRepository.findOne({ where: { name: 'moderator' } });user.roles.push(newRole);await userRepository.save(user);
// Method 3: Using QueryBuilderawait userRepository .createQueryBuilder() .relation(User, 'roles') .of(user) .add(role);Removing Relations
Section titled “Removing Relations”// Method 1: Remove specific roleconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['roles'],});
user.roles = user.roles.filter(r => r.name !== 'admin');await userRepository.save(user);
// Method 2: Remove all rolesconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['roles'],});
user.roles = [];await userRepository.save(user);
// Method 3: Using QueryBuilderawait userRepository .createQueryBuilder() .relation(User, 'roles') .of(user) .remove(role);
// Method 4: Remove and add in one operationawait userRepository .createQueryBuilder() .relation(User, 'roles') .of(user) .addAndRemove([newRole1, newRole2], [oldRole1, oldRole2]);Reading Relations
Section titled “Reading Relations”// Find user with rolesconst userWithRoles = await userRepository.findOne({ where: { id: 1 }, relations: ['roles'],});
// Find role with usersconst roleWithUsers = await roleRepository.findOne({ where: { name: 'admin' }, relations: ['users'],});
// Using QueryBuilderconst userWithRoles = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.roles', 'role') .where('user.id = :id', { id: 1 }) .getOne();
// Find users by roleconst usersWithAdminRole = await userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .where('role.name = :roleName', { roleName: 'admin' }) .getMany();28.6 Many-to-Many with Additional Columns
Section titled “28.6 Many-to-Many with Additional Columns”Creating an Entity for the Junction Table
Section titled “Creating an Entity for the Junction Table” Many-to-Many with Extra Columns ================================================================================
When you need additional columns in the junction table:
+------------------+ +------------------+ +------------------+ | User | | UserTeam | | Team | +------------------+ +------------------+ +------------------+ | id (PK) | 1:N | id (PK) | N:1 | id (PK) | | name | <------>| userId (FK) |<------->| name | | email | | teamId (FK) | | description | +------------------+ | role (extra!) | +------------------+ | joinedAt (extra!)| +------------------+
This transforms M:N into two 1:N relationships
================================================================================Junction Entity with Extra Columns
Section titled “Junction Entity with Extra Columns”@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column() email: string;
@OneToMany(() => UserTeam, (userTeam) => userTeam.user) userTeams: UserTeam[];}
// src/teams/team.entity.ts@Entity()export class Team { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column({ nullable: true }) description: string;
@OneToMany(() => UserTeam, (userTeam) => userTeam.team) userTeams: UserTeam[];
// Virtual many-to-many for convenience @ManyToMany(() => User, (user) => user.teams) users: User[];}
// src/user-teams/user-team.entity.ts@Entity()export class UserTeam { @PrimaryGeneratedColumn() id: number;
@Column() userId: number;
@Column() teamId: number;
@Column() role: string; // Extra column: user's role in team
@CreateDateColumn() joinedAt: Date; // Extra column: when user joined
@ManyToOne(() => User, (user) => user.userTeams) user: User;
@ManyToOne(() => Team, (team) => team.userTeams) team: Team;
@Column({ default: true }) isActive: boolean; // Extra column: is membership active}Working with Junction Entity
Section titled “Working with Junction Entity”import { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { UserTeam } from './user-team.entity';import { User } from '../users/user.entity';import { Team } from '../teams/team.entity';
@Injectable()export class UserTeamsService { constructor( @InjectRepository(UserTeam) private userTeamRepository: Repository<UserTeam>, @InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(Team) private teamRepository: Repository<Team>, ) {}
// Add user to team with role async addUserToTeam( userId: number, teamId: number, role: string, ): Promise<UserTeam> { const userTeam = this.userTeamRepository.create({ userId, teamId, role, }); return this.userTeamRepository.save(userTeam); }
// Update user's role in team async updateUserRole( userId: number, teamId: number, newRole: string, ): Promise<UserTeam> { const userTeam = await this.userTeamRepository.findOne({ where: { userId, teamId }, });
if (!userTeam) { throw new Error('User not in team'); }
userTeam.role = newRole; return this.userTeamRepository.save(userTeam); }
// Remove user from team async removeUserFromTeam(userId: number, teamId: number): Promise<void> { await this.userTeamRepository.delete({ userId, teamId }); }
// Get all users in team with their roles async getTeamMembers(teamId: number) { return this.userTeamRepository .createQueryBuilder('userTeam') .leftJoinAndSelect('userTeam.user', 'user') .where('userTeam.teamId = :teamId', { teamId }) .andWhere('userTeam.isActive = :isActive', { isActive: true }) .getMany(); }
// Get all teams for user with roles async getUserTeams(userId: number) { return this.userTeamRepository .createQueryBuilder('userTeam') .leftJoinAndSelect('userTeam.team', 'team') .where('userTeam.userId = :userId', { userId }) .getMany(); }
// Get users by role in team async getUsersByRole(teamId: number, role: string) { return this.userTeamRepository .createQueryBuilder('userTeam') .leftJoinAndSelect('userTeam.user', 'user') .where('userTeam.teamId = :teamId', { teamId }) .andWhere('userTeam.role = :role', { role }) .getMany(); }}28.7 Query Examples
Section titled “28.7 Query Examples”Find Users with Multiple Roles
Section titled “Find Users with Multiple Roles”// Users with both 'admin' AND 'editor' rolesconst users = await userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .where((qb) => { const subQuery = qb .subQuery() .select('ur.userId') .from('user_roles', 'ur') .innerJoin('roles', 'r', 'r.id = ur.roleId') .where('r.name IN (:...roles)', { roles: ['admin', 'editor'] }) .groupBy('ur.userId') .having('COUNT(r.id) = 2') .getQuery(); return `user.id IN ${subQuery}`; }) .getMany();Find Users with Any of the Roles
Section titled “Find Users with Any of the Roles”// Users with 'admin' OR 'editor' roleconst users = await userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .where('role.name IN (:...roles)', { roles: ['admin', 'editor'] }) .distinct(true) .getMany();Count Users per Role
Section titled “Count Users per Role”const roleUserCounts = await roleRepository .createQueryBuilder('role') .leftJoin('role.users', 'user') .select('role.id', 'roleId') .addSelect('role.name', 'roleName') .addSelect('COUNT(user.id)', 'userCount') .groupBy('role.id') .getRawMany();
// Result:// [// { roleId: 1, roleName: 'admin', userCount: '5' },// { roleId: 2, roleName: 'editor', userCount: '10' },// ]Find Users Without a Specific Role
Section titled “Find Users Without a Specific Role”// Users without 'admin' roleconst usersWithoutAdmin = await userRepository .createQueryBuilder('user') .where((qb) => { const subQuery = qb .subQuery() .select('ur.userId') .from('user_roles', 'ur') .innerJoin('roles', 'r', 'r.id = ur.roleId') .where('r.name = :roleName') .getQuery(); return `user.id NOT IN ${subQuery}`; }) .setParameter('roleName', 'admin') .getMany();28.8 Self-Referencing Many-to-Many
Section titled “28.8 Self-Referencing Many-to-Many”User Following System
Section titled “User Following System”@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column() username: string;
// Users this user follows @ManyToMany(() => User, (user) => user.followers) @JoinTable({ name: 'user_follows', joinColumn: { name: 'followerId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'followingId', referencedColumnName: 'id' }, }) following: User[];
// Users who follow this user @ManyToMany(() => User, (user) => user.following) followers: User[];}
// Usage// User 1 follows User 2const user1 = await userRepository.findOne({ where: { id: 1 } });const user2 = await userRepository.findOne({ where: { id: 2 } });
user1.following.push(user2);await userRepository.save(user1);
// Get user's followersconst userWithFollowers = await userRepository.findOne({ where: { id: 1 }, relations: ['followers'],});
// Get users that a user followsconst userWithFollowing = await userRepository.findOne({ where: { id: 1 }, relations: ['following'],});
// Count followers and followingconst user = await userRepository .createQueryBuilder('user') .leftJoin('user.followers', 'follower') .leftJoin('user.following', 'following') .select('user.id', 'userId') .addSelect('COUNT(DISTINCT follower.id)', 'followersCount') .addSelect('COUNT(DISTINCT following.id)', 'followingCount') .where('user.id = :id', { id: 1 }) .groupBy('user.id') .getRawOne();28.9 Complete Example
Section titled “28.9 Complete Example”import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn,} from 'typeorm';import { Role } from '../roles/role.entity';import { Permission } from '../permissions/permission.entity';
@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column({ unique: true }) email: string;
@Column({ select: false }) password: string;
@Column({ default: true }) isActive: boolean;
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
@ManyToMany(() => Role, (role) => role.users, { eager: false, cascade: ['insert'], }) @JoinTable({ name: 'user_roles', joinColumn: { name: 'user_id', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' }, }) roles: Role[];}
// src/roles/role.entity.tsimport { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable,} from 'typeorm';import { User } from '../users/user.entity';import { Permission } from '../permissions/permission.entity';
@Entity()export class Role { @PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) name: string;
@Column({ nullable: true }) description: string;
@ManyToMany(() => User, (user) => user.roles) users: User[];
@ManyToMany(() => Permission, (permission) => permission.roles, { eager: true, }) @JoinTable({ name: 'role_permissions', joinColumn: { name: 'role_id', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' }, }) permissions: Permission[];}
// src/permissions/permission.entity.tsimport { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';import { Role } from '../roles/role.entity';
@Entity()export class Permission { @PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) name: string;
@Column({ nullable: true }) description: string;
@ManyToMany(() => Role, (role) => role.permissions) roles: Role[];}
// src/users/users.service.tsimport { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';import { Role } from '../roles/role.entity';
@Injectable()export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(Role) private roleRepository: Repository<Role>, ) {}
async assignRoles(userId: number, roleIds: number[]): Promise<User> { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['roles'], });
if (!user) { throw new NotFoundException('User not found'); }
const roles = await this.roleRepository.findByIds(roleIds);
if (roles.length !== roleIds.length) { throw new BadRequestException('Some roles not found'); }
user.roles = roles; return this.userRepository.save(user); }
async addRole(userId: number, roleId: number): Promise<User> { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['roles'], });
if (!user) { throw new NotFoundException('User not found'); }
const role = await this.roleRepository.findOne({ where: { id: roleId } });
if (!role) { throw new NotFoundException('Role not found'); }
// Check if user already has this role const hasRole = user.roles.some(r => r.id === roleId); if (hasRole) { throw new BadRequestException('User already has this role'); }
user.roles.push(role); return this.userRepository.save(user); }
async removeRole(userId: number, roleId: number): Promise<User> { const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['roles'], });
if (!user) { throw new NotFoundException('User not found'); }
user.roles = user.roles.filter(r => r.id !== roleId); return this.userRepository.save(user); }
async getUserWithRoles(userId: number) { return this.userRepository.findOne({ where: { id: userId }, relations: ['roles', 'roles.permissions'], }); }
async getUserPermissions(userId: number): Promise<string[]> { const user = await this.userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .leftJoin('role.permissions', 'permission') .select('permission.name', 'permissionName') .where('user.id = :userId', { userId }) .getRawMany();
return user.map(u => u.permissionName).filter(Boolean); }
async hasPermission(userId: number, permissionName: string): Promise<boolean> { const result = await this.userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .leftJoin('role.permissions', 'permission') .where('user.id = :userId', { userId }) .andWhere('permission.name = :permissionName', { permissionName }) .getCount();
return result > 0; }
async getUsersByRole(roleName: string) { return this.userRepository .createQueryBuilder('user') .leftJoin('user.roles', 'role') .where('role.name = :roleName', { roleName }) .getMany(); }}28.10 Summary
Section titled “28.10 Summary” Many-to-Many Relationship Quick Reference +------------------------------------------------------------------+ | | | Decorator | Purpose | | -------------------|------------------------------------------| | @ManyToMany() | Define many-to-many relationship | | @JoinTable() | Specify junction table (owning side) | | | | Key Points | Description | | -------------------|------------------------------------------| | Owning side | Entity with @JoinTable | | Inverse side | Entity without @JoinTable | | Junction table | Auto-created table for M:N | | | | Operations | Method | | -------------------|------------------------------------------| | Add relation | user.roles.push(role); save() | | Remove relation | user.roles.filter(); save() | | QueryBuilder add | .relation().of().add() | | QueryBuilder remove| .relation().of().remove() | | | | Best Practices | Description | | -------------------|------------------------------------------| | Use junction entity| When you need extra columns | | Index FK columns | Improve join performance | | Avoid eager: true | Prevent N+1 problems | | Use distinct | Prevent duplicate results | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 29: Transactions & Concurrency
Last Updated: February 2026