One_to_many
Chapter 27: One-to-Many & Many-to-One Relationships
Section titled “Chapter 27: One-to-Many & Many-to-One Relationships”Mastering Parent-Child Entity Relationships
Section titled “Mastering Parent-Child Entity Relationships”27.1 One-to-Many Relationship Overview
Section titled “27.1 One-to-Many Relationship Overview”A one-to-many relationship connects two entities where one instance of the first entity can be associated with multiple instances of the second entity.
One-to-Many Relationship ================================================================================
+------------------+ +------------------+ | User | | Post | +------------------+ +------------------+ | id (PK) | 1:N | id (PK) | | name | <---------> | title | | email | | content | | | | authorId (FK) | +------------------+ +------------------+ | | Foreign Key References User.id
Example: - User 1 -----> Post 1, Post 2, Post 3 - User 2 -----> Post 4 - User 3 -----> (no posts)
================================================================================27.2 Basic One-to-Many Relationship
Section titled “27.2 Basic One-to-Many Relationship”Parent Entity (One Side)
Section titled “Parent Entity (One Side)”import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';import { Post } from '../posts/post.entity';
@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@Column({ unique: true }) email: string;
// One user has many posts @OneToMany(() => Post, (post) => post.author) posts: Post[];}Child Entity (Many Side - Owning Side)
Section titled “Child Entity (Many Side - Owning Side)”import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';import { User } from '../users/user.entity';
@Entity()export class Post { @PrimaryGeneratedColumn() id: number;
@Column() title: string;
@Column('text') content: string;
// Many posts belong to one user (owning side) @ManyToOne(() => User, (user) => user.posts) @JoinColumn({ name: 'authorId' }) author: User;}27.3 Database Schema
Section titled “27.3 Database Schema” Generated Database Schema ================================================================================
Table: users +------------+----------+---------------+ | Column | Type | Constraints | +------------+----------+---------------+ | id | integer | PRIMARY KEY | | name | varchar | NOT NULL | | email | varchar | UNIQUE | +------------+----------+---------------+
Table: posts +------------+----------+---------------+ | Column | Type | Constraints | +------------+----------+---------------+ | id | integer | PRIMARY KEY | | title | varchar | NOT NULL | | content | text | | | authorId | integer | FK (users.id) | +------------+----------+---------------+ | | FOREIGN KEY v users.id
Note: authorId is NOT unique (multiple posts can have same author)
================================================================================27.4 Relationship Options
Section titled “27.4 Relationship Options”Complete Options Example
Section titled “Complete Options Example”@Entity()export class Post { @PrimaryGeneratedColumn() id: number;
@Column() title: string;
// Many-to-one with all options @ManyToOne(() => User, (user) => user.posts, { // Eager loading eager: false, // Don't auto-load author
// Cascading cascade: true, // Cascade insert/update // cascade: ['insert', 'update'],
// Delete behavior onDelete: 'CASCADE', // Delete posts when user deleted // onDelete: 'SET NULL', // Set authorId to null // onDelete: 'RESTRICT', // Prevent user deletion if posts exist
// Nullability nullable: false, // authorId cannot be null
// Default value default: undefined, }) @JoinColumn({ name: 'authorId' }) author: User;}One-to-Many Options
Section titled “One-to-Many Options”@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
@OneToMany(() => Post, (post) => post.author, { // Cascading cascade: true, // Auto-save posts with user cascade: ['insert', 'update', 'remove'],
// Eager loading eager: false, // Don't auto-load posts
// Nullable (for foreign key) nullable: true, // Posts can exist without user }) posts: Post[];}27.5 Working with One-to-Many Relationships
Section titled “27.5 Working with One-to-Many Relationships”Creating Related Entities
Section titled “Creating Related Entities”// Method 1: Create parent first, then childrenconst user = await userRepository.save({ name: 'John Doe', email: 'john@example.com',});
await postRepository.save([ { title: 'Post 1', content: 'Content 1', author: user }, { title: 'Post 2', content: 'Content 2', author: user },]);
// Method 2: Create with cascadeconst user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', posts: [ { title: 'Post 1', content: 'Content 1' }, { title: 'Post 2', content: 'Content 2' }, ],});
// Method 3: Using entity instanceconst user = userRepository.create({ name: 'John Doe', email: 'john@example.com',});user.posts = [ postRepository.create({ title: 'Post 1', content: 'Content 1' }), postRepository.create({ title: 'Post 2', content: 'Content 2' }),];await userRepository.save(user);Reading Related Entities
Section titled “Reading Related Entities”// Find user with postsconst userWithPosts = await userRepository.findOne({ where: { id: 1 }, relations: ['posts'],});
// Find posts with authorconst postsWithAuthor = await postRepository.find({ relations: ['author'],});
// Using QueryBuilderconst userWithPosts = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.id = :id', { id: 1 }) .getOne();
// Select specific columnsconst userWithPostTitles = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .select(['user.id', 'user.name', 'post.id', 'post.title']) .where('user.id = :id', { id: 1 }) .getOne();Updating Related Entities
Section titled “Updating Related Entities”// Update post's authorconst post = await postRepository.findOne({ where: { id: 1 } });const newAuthor = await userRepository.findOne({ where: { id: 2 } });post.author = newAuthor;await postRepository.save(post);
// Add new post to userconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['posts'],});user.posts.push({ title: 'New Post', content: 'Content' } as Post);await userRepository.save(user);
// Replace all postsconst user = await userRepository.findOne({ where: { id: 1 } });user.posts = [ { title: 'Post 1', content: 'Content 1' } as Post, { title: 'Post 2', content: 'Content 2' } as Post,];await userRepository.save(user);Deleting Related Entities
Section titled “Deleting Related Entities”// Delete specific postawait postRepository.delete({ id: 1 });
// Delete all posts by userawait postRepository.delete({ author: { id: 1 } });
// Delete user (posts deleted via CASCADE)await userRepository.delete({ id: 1 });
// Remove post from user's collectionconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['posts'],});user.posts = user.posts.filter(p => p.id !== postId);await userRepository.save(user);27.6 Query Examples
Section titled “27.6 Query Examples”Find Users with Post Count
Section titled “Find Users with Post Count”// Using QueryBuilderconst usersWithPostCount = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .select(['user.id', 'user.name', 'user.email']) .addSelect('COUNT(post.id)', 'postCount') .groupBy('user.id') .getRawMany();
// Result:// [// { user_id: 1, user_name: 'John', user_email: 'john@example.com', postCount: '3' },// { user_id: 2, user_name: 'Jane', user_email: 'jane@example.com', postCount: '5' },// ]Find Users with Their Latest Post
Section titled “Find Users with Their Latest Post”const usersWithLatestPost = await userRepository .createQueryBuilder('user') .leftJoin( (qb) => qb .select('authorId') .addSelect('MAX(createdAt)', 'latestDate') .from(Post, 'post') .groupBy('authorId'), 'latest', 'latest.authorId = user.id' ) .leftJoin('user.posts', 'post', 'post.createdAt = latest.latestDate') .select(['user.id', 'user.name', 'post.title', 'post.createdAt']) .getRawMany();Find Posts with Author Details
Section titled “Find Posts with Author Details”const postsWithAuthor = await postRepository .createQueryBuilder('post') .leftJoinAndSelect('post.author', 'author') .orderBy('post.createdAt', 'DESC') .take(10) .getMany();
// Using find optionsconst postsWithAuthor = await postRepository.find({ relations: ['author'], order: { createdAt: 'DESC' }, take: 10,});Find Posts by Author Criteria
Section titled “Find Posts by Author Criteria”// Posts by active usersconst posts = await postRepository .createQueryBuilder('post') .leftJoin('post.author', 'author') .where('author.isActive = :isActive', { isActive: true }) .getMany();
// Posts by users with specific roleconst posts = await postRepository .createQueryBuilder('post') .leftJoin('post.author', 'author') .where('author.role = :role', { role: 'admin' }) .getMany();Pagination with Relations
Section titled “Pagination with Relations”async function getPostsByUser( userId: number, page: number, limit: number,) { const skip = (page - 1) * limit;
const [posts, total] = await postRepository .createQueryBuilder('post') .leftJoin('post.author', 'author') .where('author.id = :userId', { userId }) .orderBy('post.createdAt', 'DESC') .skip(skip) .take(limit) .getManyAndCount();
return { data: posts, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, };}27.7 Self-Referencing One-to-Many
Section titled “27.7 Self-Referencing One-to-Many”Category Hierarchy Example
Section titled “Category Hierarchy Example”@Entity()export class Category { @PrimaryGeneratedColumn() id: number;
@Column() name: string;
// Parent category @ManyToOne(() => Category, (category) => category.children) @JoinColumn({ name: 'parentId' }) parent: Category;
// Child categories @OneToMany(() => Category, (category) => category.parent) children: Category[];}
// Usageconst parentCategory = await categoryRepository.save({ name: 'Electronics',});
const childCategory = await categoryRepository.save({ name: 'Laptops', parent: parentCategory,});
// Query with childrenconst categoryWithChildren = await categoryRepository.findOne({ where: { id: 1 }, relations: ['children', 'children.children'], // Nested relations});Comment Thread Example
Section titled “Comment Thread Example”@Entity()export class Comment { @PrimaryGeneratedColumn() id: number;
@Column('text') content: string;
// Parent comment (for replies) @ManyToOne(() => Comment, (comment) => comment.replies, { nullable: true, // Root comments have no parent }) @JoinColumn({ name: 'parentId' }) parent: Comment;
// Reply comments @OneToMany(() => Comment, (comment) => comment.parent) replies: Comment[];
// Post the comment belongs to @ManyToOne(() => Post, (post) => post.comments) post: Post;
// User who wrote the comment @ManyToOne(() => User) author: User;}27.8 Complete Example
Section titled “27.8 Complete Example”import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn, Index,} from 'typeorm';import { Post } from '../posts/post.entity';import { Comment } from '../comments/comment.entity';
@Entity()export class User { @PrimaryGeneratedColumn() id: number;
@Column() @Index() name: string;
@Column({ unique: true }) @Index() email: string;
@Column({ select: false }) password: string;
@Column({ default: true }) isActive: boolean;
@CreateDateColumn() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
@OneToMany(() => Post, (post) => post.author, { cascade: ['insert'], eager: false, }) posts: Post[];
@OneToMany(() => Comment, (comment) => comment.author) comments: Comment[];}
// src/posts/post.entity.tsimport { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, CreateDateColumn, UpdateDateColumn, Index,} from 'typeorm';import { User } from '../users/user.entity';import { Comment } from '../comments/comment.entity';
@Entity()@Index(['author', 'createdAt']) // Composite indexexport class Post { @PrimaryGeneratedColumn() id: number;
@Column() @Index() title: string;
@Column('text') content: string;
@Column({ default: 0 }) viewCount: number;
@Column({ default: true }) isPublished: boolean;
@CreateDateColumn() @Index() createdAt: Date;
@UpdateDateColumn() updatedAt: Date;
@ManyToOne(() => User, (user) => user.posts, { eager: false, onDelete: 'CASCADE', }) @JoinColumn({ name: 'authorId' }) author: User;
@OneToMany(() => Comment, (comment) => comment.post, { cascade: true, }) comments: Comment[];}
// src/posts/posts.service.tsimport { Injectable, NotFoundException } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { Post } from './post.entity';import { User } from '../users/user.entity';
@Injectable()export class PostsService { constructor( @InjectRepository(Post) private postRepository: Repository<Post>, @InjectRepository(User) private userRepository: Repository<User>, ) {}
async create(userId: number, postData: any): Promise<Post> { const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) { throw new NotFoundException('User not found'); }
const post = this.postRepository.create({ ...postData, author: user, });
return this.postRepository.save(post); }
async findAll(page: number, limit: number) { const skip = (page - 1) * limit;
const [data, total] = await this.postRepository.findAndCount({ relations: ['author'], order: { createdAt: 'DESC' }, skip, take: limit, select: { id: true, title: true, createdAt: true, author: { id: true, name: true, }, }, });
return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; }
async findByAuthor(authorId: number, page: number, limit: number) { const skip = (page - 1) * limit;
const [data, total] = await this.postRepository .createQueryBuilder('post') .leftJoin('post.author', 'author') .where('author.id = :authorId', { authorId }) .orderBy('post.createdAt', 'DESC') .skip(skip) .take(limit) .getManyAndCount();
return { data, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; }
async findOneWithDetails(id: number) { const post = await this.postRepository .createQueryBuilder('post') .leftJoinAndSelect('post.author', 'author') .leftJoinAndSelect('post.comments', 'comments') .leftJoinAndSelect('comments.author', 'commentAuthor') .where('post.id = :id', { id }) .getOne();
if (!post) { throw new NotFoundException('Post not found'); }
// Increment view count await this.postRepository .createQueryBuilder() .update(Post) .set({ viewCount: () => 'viewCount + 1' }) .where('id = :id', { id }) .execute();
return post; }
async getPostStats(authorId: number) { const stats = await this.postRepository .createQueryBuilder('post') .leftJoin('post.author', 'author') .where('author.id = :authorId', { authorId }) .select('COUNT(post.id)', 'totalPosts') .addSelect('SUM(post.viewCount)', 'totalViews') .addSelect('AVG(post.viewCount)', 'avgViews') .getRawOne();
return stats; }}27.9 Summary
Section titled “27.9 Summary” One-to-Many Relationship Quick Reference +------------------------------------------------------------------+ | | | Decorator | Purpose | | -------------------|------------------------------------------| | @OneToMany() | Define one-to-many (parent side) | | @ManyToOne() | Define many-to-one (child/owning side) | | @JoinColumn() | Specify foreign key column | | | | Key Points | Description | | -------------------|------------------------------------------| | Owning side | Child entity (has foreign key) | | Inverse side | Parent entity (has collection) | | cascade | Auto-save related entities | | onDelete | Action on parent delete | | | | Best Practices | Description | | -------------------|------------------------------------------| | Index FK columns | Improve join performance | | Use pagination | Prevent memory issues | | Avoid eager: true | Prevent N+1 problems | | Use QueryBuilder | For complex queries | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 28: Many-to-Many Relationships
Last Updated: February 2026