Skip to content

Relationships_overview


TypeORM supports four main types of relationships between entities.

Entity Relationship Types
================================================================================
Relationships
|
+-----------+-----------+-----------+-----------+
| | | | |
v v v v v
+-------+ +-------+ +-----------+ +-------+ +-------+
| 1:1 | | 1:N | | N:1 | | N:N | | Self |
|One-to-| |One-to-| | Many-to- | |Many-to| | Ref |
| One | | Many | | One | | Many | | |
+-------+ +-------+ +-----------+ +-------+ +-------+
================================================================================

Each entity instance is related to exactly one instance of another entity.

One-to-One Relationship
+------------------------------------------------------------------+
| |
| User Profile |
| +------------------+ +------------------+ |
| | id (PK) | | id (PK) | |
| | email | 1:1 | bio | |
| | password | <------> | avatar | |
| | | | userId (FK, UQ) | |
| +------------------+ +------------------+ |
| |
| Each user has exactly one profile |
| Each profile belongs to exactly one user |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Profile } from './profile.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
// One-to-One: User has one Profile
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;
}
@Entity('profiles')
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@Column()
avatar: string;
// Foreign key column
@Column({ unique: true })
userId: number;
// One-to-One: Profile belongs to User
@OneToOne(() => User, user => user.profile)
@JoinColumn({ name: 'userId' })
user: User;
}

One entity instance can be related to multiple instances of another entity.

One-to-Many Relationship
+------------------------------------------------------------------+
| |
| User Post |
| +------------------+ +------------------+ |
| | id (PK) | | id (PK) | |
| | email | 1:N | title | |
| | name | -------> | content | |
| | | | authorId (FK) | |
| +------------------+ +------------------+ |
| ^ | |
| | N:1 | |
| +----------------------------+ |
| |
| One user can have many posts |
| Each post belongs to one user |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// One-to-Many: User has many Posts
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: 'text' })
content: string;
// Foreign key column
@Column()
authorId: number;
// Many-to-One: Post belongs to User
@ManyToOne(() => User, user => user.posts)
@JoinColumn({ name: 'authorId' })
author: User;
}

The inverse of One-to-Many. Multiple instances of one entity relate to one instance of another.

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('comments')
export class Comment {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'text' })
content: string;
// Foreign key column
@Column()
authorId: number;
// Many-to-One: Comment belongs to User
@ManyToOne(() => User, user => user.comments)
@JoinColumn({ name: 'authorId' })
author: User;
}

Multiple instances of one entity can be related to multiple instances of another entity.

Many-to-Many Relationship
+------------------------------------------------------------------+
| |
| Student Course |
| +------------------+ +------------------+ |
| | id (PK) | | id (PK) | |
| | name | | title | |
| | email | | credits | |
| +------------------+ +------------------+ |
| | ^ |
| | | |
| +-------------+--------------+ |
| | |
| v |
| +------------------+ |
| | student_courses | <-- Junction Table |
| +------------------+ |
| | studentId (PK,FK)| |
| | courseId (PK,FK) | |
| +------------------+ |
| |
| One student can enroll in many courses |
| One course can have many students |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from './course.entity';
@Entity('students')
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Many-to-Many: Student has many Courses
@ManyToMany(() => Course, course => course.students)
@JoinTable() // Creates junction table
courses: Course[];
}
@Entity('courses')
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
// Many-to-Many: Course has many Students
@ManyToMany(() => Student, student => student.courses)
students: Student[];
}

Relationship Decorators
+------------------------------------------------------------------+
| |
| Decorator | Purpose |
| -------------------|------------------------------------------|
| @OneToOne() | One-to-one relationship |
| @OneToMany() | One-to-many relationship |
| @ManyToOne() | Many-to-one relationship |
| @ManyToMany() | Many-to-many relationship |
| @JoinColumn() | Specify foreign key column |
| @JoinTable() | Specify junction table (M:N) |
| |
+------------------------------------------------------------------+

import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Profile } from './profile.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
// Basic JoinColumn
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
// Creates column: profileId
// Custom column name
@OneToOne(() => Profile)
@JoinColumn({ name: 'profile_id' })
profile: Profile;
// Creates column: profile_id
// Multiple join columns (composite key)
@OneToOne(() => Profile)
@JoinColumn([
{ name: 'profile_id', referencedColumnName: 'id' },
{ name: 'profile_type', referencedColumnName: 'type' },
])
profile: Profile;
// With foreign key constraints
@OneToOne(() => Profile)
@JoinColumn({
name: 'profileId',
referencedColumnName: 'id',
foreignKeyConstraintName: 'fk_user_profile',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
profile: Profile;
}

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from './course.entity';
@Entity('students')
export class Student {
@PrimaryGeneratedColumn()
id: number;
// Basic JoinTable
@ManyToMany(() => Course, course => course.students)
@JoinTable()
courses: Course[];
// Creates table: student_courses
// Custom table name
@ManyToMany(() => Course, course => course.students)
@JoinTable({ name: 'enrollments' })
courses: Course[];
// Creates table: enrollments
// Custom column names
@ManyToMany(() => Course, course => course.students)
@JoinTable({
name: 'enrollments',
joinColumn: {
name: 'student_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'course_id',
referencedColumnName: 'id',
},
})
courses: Course[];
// Creates table: enrollments
// Columns: student_id, course_id
// With constraints
@ManyToMany(() => Course, course => course.students)
@JoinTable({
name: 'enrollments',
joinColumn: {
name: 'studentId',
referencedColumnName: 'id',
foreignKeyConstraintName: 'fk_enrollment_student',
},
inverseJoinColumn: {
name: 'courseId',
referencedColumnName: 'id',
foreignKeyConstraintName: 'fk_enrollment_course',
},
})
courses: Course[];
}

Relations are loaded automatically with the entity.

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Eager loading - always loaded
@OneToMany(() => Post, post => post.author, { eager: true })
posts: Post[];
}
// Usage - posts are automatically loaded
const user = await userRepository.findOne({ where: { id: 1 } });
console.log(user.posts); // Already loaded

Relations are loaded on demand (using Promises).

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Lazy loading - loaded on demand
@OneToMany(() => Post, post => post.author, { lazy: true })
posts: Promise<Post[]>;
}
// Usage - posts loaded when accessed
const user = await userRepository.findOne({ where: { id: 1 } });
console.log(user.posts); // Promise
console.log(await user.posts); // Loaded now

Load relations explicitly using find options.

// Using relations option
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['posts', 'posts.comments'],
});
// Using join option
const user = await userRepository.findOne({
where: { id: 1 },
join: {
alias: 'user',
leftJoinAndSelect: ['user.posts', 'posts.comments'],
},
});

Cascade options determine how operations on an entity affect related entities.

Cascade Options
+------------------------------------------------------------------+
| |
| Option | Effect |
| ----------------|--------------------------------------------- |
| CASCADE | Insert, Update, Delete propagate |
| INSERT | Insert propagates to related entities |
| UPDATE | Update propagates to related entities |
| REMOVE | Delete propagates to related entities |
| DETACH | Remove relation without deleting |
| SOFT_REMOVE | Soft delete propagates |
| RECOVER | Recover propagates |
| |
+------------------------------------------------------------------+
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Cascade all operations
@OneToMany(() => Post, post => post.author, {
cascade: true,
})
posts: Post[];
// Cascade specific operations
@OneToMany(() => Comment, comment => comment.author, {
cascade: ['insert', 'update'],
})
comments: Comment[];
}
// Usage with cascade
const user = new User();
user.name = 'John';
user.posts = [
{ title: 'Post 1', content: 'Content 1' },
{ title: 'Post 2', content: 'Content 2' },
];
// Saves user AND posts (because cascade: true)
await userRepository.save(user);

When a related entity is removed from a collection, it gets deleted.

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Post, post => post.author, {
orphanRemoval: true,
})
posts: Post[];
}
// Usage
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['posts'],
});
// Remove post from collection
user.posts = user.posts.filter(p => p.id !== 1);
// Save user - post with id=1 is DELETED
await userRepository.save(user);

An entity can have relationships to itself.

Self-Referencing One-to-Many (Tree Structure)

Section titled “Self-Referencing One-to-Many (Tree Structure)”
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
@Entity('categories')
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Parent category
@ManyToOne(() => Category, category => category.children)
@JoinColumn({ name: 'parentId' })
parent: Category;
@Column({ nullable: true })
parentId: number;
// Child categories
@OneToMany(() => Category, category => category.parent)
children: Category[];
}
// Usage
const parent = new Category();
parent.name = 'Electronics';
await categoryRepository.save(parent);
const child = new Category();
child.name = 'Phones';
child.parent = parent;
await categoryRepository.save(child);
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Users this user follows
@ManyToMany(() => User, user => user.followers)
@JoinTable({
name: 'user_follows',
joinColumn: { name: 'followerId' },
inverseJoinColumn: { name: 'followingId' },
})
following: User[];
// Users following this user
@ManyToMany(() => User, user => user.following)
followers: User[];
}

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
// Relation with default value
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'authorId' })
author: User | null;
@Column({ nullable: true })
authorId: number | null;
}

Relationship Best Practices
+------------------------------------------------------------------+
| |
| 1. Always define both sides of a relationship |
| @OneToMany + @ManyToOne |
| @OneToOne + @OneToOne |
| @ManyToMany + @ManyToMany |
| |
| 2. Use eager loading sparingly |
| - Can cause N+1 problems |
| - Use for small, always-needed relations |
| |
| 3. Be careful with cascade: true |
| - Can accidentally delete data |
| - Use specific cascade options |
| |
| 4. Use orphanRemoval for child entities |
| - Ensures no orphaned records |
| |
| 5. Consider lazy loading for large relations |
| - Reduces initial query size |
| |
+------------------------------------------------------------------+

Relationship Quick Reference
+------------------------------------------------------------------+
| |
| Relationship | Decorators | Table Structure |
| ----------------|-------------------|---------------------------|
| One-to-One | @OneToOne | FK on one side |
| One-to-Many | @OneToMany | FK on "many" side |
| Many-to-One | @ManyToOne | FK on this side |
| Many-to-Many | @ManyToMany | Junction table |
| Self-Reference | @ManyToOne | FK to same table |
| | @OneToMany | |
| |
+------------------------------------------------------------------+

Chapter 10: Advanced Entity Patterns


Last Updated: February 2026