How to define and use relationship mappings in TypeORM? Detailed configuration for one-to-one, one-to-many, and many-to-many relationships
TypeORM provides four main types of relationship mappings, each with its specific use cases and configuration methods. Understanding these relationship mappings is crucial for building complex data models.
Four Relationship Types
1. One-to-One
A one-to-one relationship represents a unique correspondence between two entities. For example, a user can have only one profile.
typescript@Entity() export class Profile { @PrimaryGeneratedColumn() id: number; @Column() gender: string; @Column() bio: string; @OneToOne(() => User, user => user.profile) @JoinColumn() user: User; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToOne(() => Profile, profile => profile.user, { cascade: true }) profile: Profile; }
Key Points:
- Use
@OneToOne()decorator to define the relationship - Use
@JoinColumn()on the owning side to specify the foreign key column cascade: trueallows cascade operations (save, delete, etc.)
2. One-to-Many / Many-to-One
One-to-many relationships are the most common type. For example, a user can publish multiple posts.
typescript@Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() content: string; @ManyToOne(() => User, user => user.posts) author: User; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[]; }
Key Points:
@ManyToOne()is placed on the "many" side and contains the foreign key@OneToMany()is placed on the "one" side and doesn't need@JoinColumn()- Foreign key is automatically added to the "many" side table
3. Many-to-Many
Many-to-many relationships require an intermediate table to connect two entities. For example, a post can have multiple tags, and a tag can belong to multiple posts.
typescript@Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToMany(() => Post, post => post.tags) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToMany(() => Tag, tag => tag.posts, { cascade: true }) @JoinTable() tags: Tag[]; }
Key Points:
- Use
@JoinTable()on one side of the relationship to define the intermediate table - Intermediate table is automatically created with two foreign keys
- Can customize intermediate table name and column names
Relationship Configuration Options
Eager and Lazy Loading
typescript@Entity() export class User { @OneToMany(() => Post, post => post.author, { eager: true // Load related data immediately }) posts: Post[]; } // Or use lazy loading @Entity() export class User { @OneToMany(() => Post, post => post.author) posts: Promise<Post[]>; }
Cascade Operations
typescript@OneToMany(() => Post, post => post.author, { cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover'] }) posts: Post[];
Cascade operation options:
insert: Automatically save child entities when saving parent entityupdate: Automatically update child entities when updating parent entityremove: Automatically delete child entities when deleting parent entitysoft-remove: Soft deleterecover: Recover soft-deleted entities
OnDelete and OnUpdate
typescript@ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE', // Cascade delete posts when user is deleted onUpdate: 'CASCADE' // Cascade update post author ID when user ID is updated }) author: User;
Relationship Queries
Using FindOptions to Query Related Data
typescriptconst userRepository = dataSource.getRepository(User); // Load related data const users = await userRepository.find({ relations: ['posts', 'profile'] }); // Conditional query on related data const usersWithPosts = await userRepository.find({ relations: { posts: true }, where: { posts: { title: Like('%TypeORM%') } } });
Using QueryBuilder
typescriptconst users = await dataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .leftJoinAndSelect('user.profile', 'profile') .where('post.title = :title', { title: 'TypeORM Guide' }) .getMany();
Custom Relationships
Custom JoinTable
typescript@ManyToMany(() => Tag, tag => tag.posts) @JoinTable({ name: 'post_tags', joinColumn: { name: 'postId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'tagId', referencedColumnName: 'id' } }) tags: Tag[];
Custom JoinColumn
typescript@ManyToOne(() => User, user => user.posts) @JoinColumn({ name: 'author_id', referencedColumnName: 'id' }) author: User;
Best Practices
- Choose relationship types wisely: Select the most appropriate relationship type based on business requirements
- Avoid overusing Eager loading: May cause N+1 query problems
- Use cascade delete carefully: Ensure important data is not accidentally deleted
- Optimize queries with indexes: Add indexes to foreign key columns
- Consider performance impact: Complex relationship queries may affect performance
TypeORM's relationship mapping system provides powerful and flexible ways to handle relationships between entities. Mastering these concepts is crucial for building efficient and maintainable applications.