ManyToMany를 custom으로 만들어서 처리가 필요한 경우가 있습니다.
공식문서를 보고 만들다보면 아쉬운 점이 있어 관련되서 짧게 이야기 하고자 합니다.
또한 아래에서 예시로 드는 내용은 Mysql 기준인 점 참고부탁드립니다.
개발을 하다 보면 N:N(다대다) 관계를 줘야할 때가 있습니다.
예를 들어 웹툰 서비스가 있다고 했을 때 공동 작가라는 개념이 있으면 웹툰과 작가는 N:N 관계를 가집니다.
공식문서에 나와있는 예제를 위 예시로 살짝 바꿔보면 아래와 같습니다.
@Entity()
export class Webtoon {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToMany(() => Author, (author) => author.webtoons)
authors: Author[]
}
@Entity()
export class Author {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToMany(() => Webtoon, (webtoon) => webtoon.authors)
@JoinTable()
webtoons: Webtoon[]
}
위 예제를 보면 ManyToMany 를 통해 Webtoon과 Author에 N:N 관계를 가진 것을 볼 수 있습니다.
다만, 의아한 점은 관계형 데이터베이스는 테이블 2개만으로는 N:N 관계를 표현할 수 없습니다. 하지만 저희는 2개의 테이블만 작성을 했는데, N:N 관계가 됐습니다.
위 코드를 마이그레이션 돌리면 아래와 같이 Typeorm이 N:N 관계를 풀어주는 연결 테이블 하나를 같이 만듭니다.
제가 코드로 선언하지 않은 webtoon_authors_author 테이블을 SQL 제너레이터로 보면 아래와 같습니다.
create table webtoon_authors_author
(
webtoonId int not null,
authorId int not null,
primary key (webtoonId, authorId),
constraint FK_4d1bfb9ab2446b1db70bf6bf72c
foreign key (authorId) references author (id),
constraint FK_e1571e6b785a27cd9cd912ce3b3
foreign key (webtoonId) references webtoon (id)
on update cascade on delete cascade
);
create index IDX_4d1bfb9ab2446b1db70bf6bf72
on webtoon_authors_author (authorId);
create index IDX_e1571e6b785a27cd9cd912ce3b
on webtoon_authors_author (webtoonId);
위와 같이 N:N 관계를 지정하면 몇몇 단점이 있지만, 해당 글에서는 하나의 단점만 가지고 이야기 해보려고 합니다.
웹툰 서비스에서 작가이지만 메인 작가, 보조 작가로도 분리할 수 있습니다.
ex) A 작가는 C 웹툰에서는 메인이지만 D 웹툰에서는 보조 작가일 수도 있습니다.
이러한 분리를 위에서 생성된 테이블만으로는 처리가 쉽지 않습니다.
그렇기에 Typeorm 공식문서를 보면 Custom을 하는 예제가 있습니다.
예제를 위 예시로 코드화 하면 아래와 같습니다.
@Entity()
export class WebtoonToAuthor {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Webtoon, (webtoon) => webtoon.webtoonToAuthors)
@JoinColumn({ name: 'webtoonId' })
webtoon: Webtoon;
@ManyToOne(() => Author, (author) => author.webtoonToAuthors)
@JoinColumn({ name: 'authorId' })
author: Author;
@Column()
webtoonId: number;
@Column()
authorId: number;
@Column()
role: string;
}
// webtoon.ts
@OneToMany(() => WebtoonToAuthor, (webtoonToAuthor) => webtoonToAuthor.webtoon)
webtoonToAuthors: WebtoonToAuthor[];
// author.ts
@OneToMany(() => WebtoonToAuthor, (webtoonToAuthor) => webtoonToAuthor.author)
webtoonToAuthors: WebtoonToAuthor[];
이런식으로 ManyToOne과 OneToMany를 활용해서 직접 연결 테이블을 구현해 role 권한을 부여 및 컨트롤 할 수 있습니다.
하지만, 공식문서의 예제처럼 실제 코드를 작성했을 때는 한 가지 문제점이 있다고 생각합니다.
위 예제처럼 작성하는 경우 어떠한 문제가 발생할 수 있을까요?
DB에 중복 데이터가 들어갈 수 있다는 문제가 있습니다.
기본적인 ManyToMany로 테이블을 만드는 경우 PK 설정을 아래와 같이 하기 때문에 중복이 발생하지 않습니다.
primary key (webtoonId, authorId)
공식문서의 예제를 기반으로 만들게 되면 A작가가 C작품의 메인작가 이면서 보조작가인 식으로 데이터가 들어갈 수 있습니다.
물론 서비스의 특성 상 그렇게 설정할 수 도 있긴 하지만, 일반적으로 N:N 관계에서 중복이 없기 때문에 그 부분은 고려하지 않겠습니다.
이 문제를 해결할 수 있는 여러 방법이 있지만 해당 글에서는 하나의 방법만 소개하겠습니다.
Unique
를 통해 중복 데이터를 테이블 단에서 제한하면 위 문제점을 예방할 수 있습니다.
@Unique(['webtoonId', 'authorId'])
@Entity()
export class WebtoonToAuthor {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Webtoon, (webtoon) => webtoon.webtoonToAuthors)
@JoinColumn({ name: 'webtoonId' })
webtoon: Webtoon;
@ManyToOne(() => Author, (author) => author.webtoonToAuthors)
@JoinColumn({ name: 'authorId' })
author: Author;
@Column()
webtoonId: number;
@Column()
authorId: number;
@Column()
role: string;
}
이런 식으로 DB 단에서 중복을 예방하면, 서비스의 안정성이 조금 더 올라갈 것이라고 생각합니다.