다대다(N:M) 관계는 데이터베이스 설계에서 두 엔티티가 서로 다수의 인스턴스와 관계를 가질 수 있는 경우를 의미한다. 예를 들어, 학생과 수업 사이의 관계를 생각해볼 수 있다. 한 학생은 여러 수업을 들을 수 있고, 한 수업도 여러 학생이 들을 수 있다.
ManyToMany 관계에는 두 엔티티를 연결하는 중간테이블이 생성되며, @JoinTable()을 사용하여 중간테이블 명과 참조 컬럼을 명시해줄 수 있다.
// import 생략
@Entity()
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Course, (course) => course.students)
@JoinTable()
courses: Course[];
}
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Student, (student) => student.courses)
students: Student[];
}
@JoinTable()은 관계의 한쪽(소유)에 넣어야 하며, student_courses_course 라는 중간 테이블이 생성되는 것을 확인할 수 있다. 또한, 참조 컬럼은 각 엔티티의 기본키이다.
// import 생략
@Entity()
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Course, (course) => course.students)
@JoinTable({
name: 'student_course_relation',
joinColumn: { name: 'studentId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'courseId', referencedColumnName: 'id' },
})
courses: Course[];
}
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Student, (student) => student.courses)
students: Student[];
}
name 옵션으로 중간 테이블 명을 지정하고, 참조할 컬럼과 명은 joinColumn과 inverseJoinColumn을 사용해 지정해주면 된다.
위의 경우는 student_course_relation 이라는 명으로 중간 테이블이 생성되고, 현재 엔티티의 id 컬럼을 참조한 외래 키의 명이 studentId이다. 또, 상대 엔티티인 Course 엔티티의 id 컬럼을 참조한 외래 키의 명이 courseId 이다.
@JoinTable() 기본 옵션
- name: 중간 테이블의 이름 지정(지정하지 않으면 기본적으로 엔티티 이름을 조합하여 생성)
- joinColumn: 현재 엔티티에서 중간 테이블로 가는 방향의 외래 키 지정
- name: 외래 키 컬럼의 이름 지정
- referencedColumnName: 현재 엔티티의 컬럼을 참조
- inverseJoinColumn: 상대 엔티티에서 중간 테이블로 가는 방향의 외래 키 지정
- name: 외래 키 컬럼의 이름 지정
- referencedColumnName: 상대 엔티티의 컬럼을 참조
// import 생략
@Entity()
export class Student {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Course, (course) => course.students, {
cascade: true,
eager: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
@JoinTable({
name: 'student_course_relation',
joinColumn: { name: 'studentId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'courseId', referencedColumnName: 'id' },
})
courses: Course[];
}
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Student, (student) => student.courses)
students: Student[];
}
Student와 Course 두 군데에 casecade와 eager를 작성하면 에러 발생한다.
- cascade: true
부모 엔티티에 수행되는 작업을 자식 엔티티에도 자동으로 적용하는 역할을 한다. 부모 엔티티에서 실행되는 생성, 수정, 삭제 등의 작업이 자식엔티티에도 영향이 간다.- eager: true
엔티티를 로드할 때 관련된 엔티티를 자동으로 함께 로드하는 역할을 한다. 위 코드에선 Student 데이터를 조회할때 연결된 Course 데이터가 함께 조회되고, Course 데이터를 조회할때는 Course 데이터만 조회가 된다.- onDelete: 'CASCADE'
onDelete 옵션은 참조된 외래 키의 레코드가 삭제될 때, 해당 외래 키를 어떻게 처리할지를 정의하는 역할을 하며, Student가 삭제될 때 student_course_relation에 해당 레코드도 함께 삭제된다.- onUpdate: 'CASCADE'
onUpdate 옵션은 참조된 외래 키가 업데이트될 때, 해당 외래 키를 어떻게 처리할지를 정의하는 역할을 한다. 만약 Student의 id가 수정되면 student_course_relation에도 적용이 된다.
만약 Course 엔티티를 삭제하거나 수정한 경우에 중간테이블의 외래 키 처리 방식을 정의하고싶다면?
@Entity() export class Course { ... @ManyToMany(() => Student, (student) => student.courses, { onUpdate: 'CASCADE', onDelete: 'CASCADE', }) students: Student[]; }
위와 같이 작성해주면 Course 엔티티가 삭제되거나 업데이트 된 경우, 중간 테이블의 데이터에 영향을 미치게된다.