TypeORM을 사용한 엔티티 관계 정의 중 ManyToOne, OneToMany에 관해 알아보자.
일대다(1:N) 관계는 한 쪽 엔티티가 관계를 맺은 엔티티 쪽의 여러 객체를 가질 수 있는 것을 의미한다. 즉, 테이블A 한 개의 행이 테이블B 여러 개의 행과 연결되는 관계를 말한다. 이해하기 쉽게 예를 들자면, 국가와 도시가 있다. 하나의 국가(1)은 여러 개의 도시(N)를 가질 수 있다.
@ManyToOne(), @OneToMany() 관계에서 @ManyToOne() 데코레이터가 작성되는 엔티티가 자식엔티티이다. 1:N 관계에서는 일반적으로 자식 엔티티에 @JoinColumn()를 작성하여 외래키를 연결한다.
NestJS에서 TypeORM을 이용하여 entity 관계를 설정하는 방법은 아래와 같다.
// import 생략
@Entity()
export class Country {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@OneToMany(() => City, (city) => city.country, {
cascade: true,
eager: true
})
cities: City[]
}
@Entity()
export class City {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToOne(() => Country, (country) => country.cities, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
@JoinColumn({name: 'country_id', referencedColumnName: 'id'})
country: Country
}
- eager
- 관련된 엔티티를 즉시 로드하는 기능.
- 부모 엔티티를 조회할 때, TypeORM의 find() 또는 findOne() 메서드를 사용하여 relations 옵션 없이 바로 연결된 자식 엔티티도 함께 로드할 수 있다.
- createQueryBuilder()에서는 적용되지 않는다.
- cascade
- 부모 엔티티의 변경사항이 자식 엔티티에 자동으로 전파되는 기능.
- 해당 옵션을 통해 부모 엔티티의 생성, 업데이트, 삭제 작업이 자식 엔티티에도 자동으로 적용될 수 있다.
- save() 메서드를 사용할 때, 부모 엔티티를 생성함과 동시에 자식엔티티도 함께 생성할 수 있지만, createQueryBuilder()에서는 이 옵션이 적용되지 않는다.
- 부모 엔티티의 변경 사항이 자식 엔티티의 외래 키에 어떤 영향을 미칠지는 onDelete와 onUpdate 옵션을 사용하여 정의한다. 이 옵션들은 TypeORM에서 ManyToOne과 OneToMany 관계를 설정할 때 외래 키의 동작을 제어하는 데 사용된다.
- onDelete
- 외래 키가 참조하는 레코드가 삭제될 때, 외래 키의 처리 방식을 정의하는 옵션.
- 다른 테이블의 레코드와의 일관성을 유지하기 위해 사용된다.
- 일반적으로 사용되는 옵션에는 CASCADE(자동 삭제), RESTRICT(삭제 거부), SET NULL(외래키 NULL로 설정), NO ACTION(아무 작업 X), SET DEFAULT(기본값) 등이 있다.
- onUpdate
- 외래 키가 참조하는 키 값이 업데이트될 때, 외래 키의 처리 방식을 정의하는 옵션.
- 외래 키가 참조하는 테이블의 레코드가 업데이트될 때 필요한 조치를 지정한다.
- CASCADE(자동 업데이트), RESTRICT(참조된 테이블의 레코드가 없을때만 업데이트 가능), SET NULL(외래키 NULL로 설정), NO ACTION(아무 작업 X)과 같은 옵션을 설정할 수 있다.
@JoinColumn()을 작성하면 상대 엔티티의 기본키를 참조하며, 참조할 컬럼을 직접 지정(referencedName)할 수 있고, 외래키 명(name)도 지정할 수 있다.
단, 참조할 컬럼은 기본키이거나 unique한 값이어야 한다.
아래 코드는 City 엔티티가 Country 엔티티의 name 컬럼을 외래키로 참조하는 예제이다.
// import 생략
@Entity()
export class Country {
@PrimaryGeneratedColumn()
id: number
@Column({ unique: true })
name: string
@OneToMany(() => City, (city) => city.country, {
cascade: true,
eager: true
})
cities: City[]
}
@Entity()
export class City {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToOne(() => Country, (country) => country.cities, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
@JoinColumn({name: 'country_name', referencedColumnName: 'name'})
country: Country
}
@JoinColumn()을 생략할 수도 있는데, 생략하면 TypeORM은 자동으로 외래키를 생성하고 관련된 컬럼을 참조하게 된다.(Country 엔티티의 기본키인 id가 참조됨)
또한, ManyToOne()의 경우 관계 정의가 필수적이지만, OneToMany()의 경우에는 생략이 가능하다.
// import 생략
@Entity()
export class Country {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
}
@Entity()
export class City {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToOne(() => Country, (country) => country.cities, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
country: Country
}
[참고자료]