본문 바로가기

Project/TypeORM

[번역] ManyToMany Relations

ManyToMany Relations에 관한 내용을 번역과 동시에 제 생각을 넣은 글입니다. 더 정확한 사실을 알고 싶다면 해당 링크를 방문해 주세요. 번역 시작하겠습니다.

내일부터 휴가

A엔티티와 B엔티티를 갖고 있는 상태로 가정하겠습니다.

Many-to-many는 A 엔티티가 B의 여러 인스턴스를 갖고 있고, B는 A의 여러 인스턴스를 갖고 있는 관계를 의미하는데요. 우리는 Question과 Categoy 엔티티로 예시를 들어볼 겁니다. A question은 여러 카테고리를 가질 수 있고, 각 카테고리는 여러 question들을 가질 수 있습니다. 아래는 Question와 Category 엔티티입니다.

Category Entity

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string
}

Question Entity

import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany(() => Category)
    @JoinTable()
    categories: Category[]
}

위 엔티티를 보자면 @JoinTable이 Question 엔티티에 작성되어있는데요? 이는 필수 조건 중에 하나입니다. 그리고 우리는 한쪽 엔티티에만 작성해야만 합니다.

위 예시를 따르면 아래의 테이블이 생성됩니다.

+-------------+--------------+----------------------------+
|                        category                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        question                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
| text        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|              question_categories_category               |
+-------------+--------------+----------------------------+
| questionId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
| categoryId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

ManyToMany 관계에서 저장방식

Cascades를 설정했다면, 우리는 단 한번의 save를 통해 관계를 저장할 수 있습니다. 아래의 예시와 같습니다.

const category1 = new Category()
category1.name = "animals"
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = "zoo"
await dataSource.manager.save(category2)

const question = new Question()
question.title = "dogs"
question.text = "who let the dogs out?"
question.categories = [category1, category2]
await dataSource.manager.save(question)

ManyToMany 관계에서 삭제방식

Cascades를 설정했다면, 우리는 이 관계를 단 한 번의 save를 통해서 삭제할 수 있습니다. 그리고 두 레코드 사이에 many-yo-many 관계를 삭제하기 위해선, 일치하는 필드로부터 삭제하고 레코드를 저장해야 합니다. 아래의 예시와 같습니다.

const question = await dataSource.getRepository(Question).findOne({
    relations: {
        categories: true,
    },
    where: { id: 1 }
})
question.categories = question.categories.filter((category) => {
    return category.id !== categoryToRemove.id
})
await dataSource.manager.save(question)

이거는 오직 조인테이블에만 레코드를 삭제할 것입니다. 그리고 Question와 CategoryToRemove 레코드들은 여전히 남아있을 겁니다.

Cascade를 사용해 관계를 Soft Delete하는 방식

soft delete라는 것을 아시나요? 레코드에서 삭제시키는 것이 아닌 false로 표시해 삭제하지 않는 것입니다. 이유는 나중에 글로 작성해 보도록 하겠습니다.

이 예시는 soft delete방식을 어떻게 cacsding으로 하는지 보여줍니다.

const category1 = new Category()
category1.name = "animals"

const category2 = new Category()
category2.name = "zoo"

const question = new Question()
question.categories = [category1, category2]
const newQuestion = await dataSource.manager.save(question)

await dataSource.manager.softRemove(newQuestion)

이 예제에서 우리는 category1, category2에게 save 또는 softRemove 를 사용하지 않았는데, 왜냐하면 자동으로 저장될 것이고 soft delete 될 것이기 때문입니다. 어떻게 되냐면, cascade 관계 옵션은 아래와 같이 설정을 해줍니다.

import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @ManyToMany(() => Category, (category) => category.questions, {
        cascade: true,
    })
    @JoinTable()
    categories: Category[]
}

 Many-to-many 관계 데이터 불러오기 방식

Categort들과 함께 Question을 불러오기 위해서 FindOptions에서는 관계를 무조건 명시해야합니다. 아래의 예시처럼 말이에요.

const questionRepository = dataSource.getRepository(Question)
const questions = await questionRepository.find({
    relations: {
        categories: true,
    },
})

또는, 조인할 때 쿼리빌더를 사용할 수도 있습니다.

const questions = await dataSource
    .getRepository(Question)
    .createQueryBuilder("question")
    .leftJoinAndSelect("question.categories", "category")
    .getMany()

관계에서 eager loading을 사용한다면 우리는 find 명령을 할때 굳이 관계를 명시할 필요는 없습니다. 왜냐하면 항상 자동으로 같이 불러오기 때문인데요. 만약 QueryBuilder를 사용한다면? 관계를 사용하지 않아도 됩니다. 그냥 leftJoinAndSelect를 사용해주기만 하면 됩니다.

양방향(Bi-directional) 관계

관계는 단방향 양방향 모두 가능한데, 단뱡향은 한쪽 엔티티에만 작성을 해주면 됩니다. 그리고 양방향은 양쪽에 다 작성해 주면 됩니다. 우리는 단방향과 양방향을 만들어 볼 수 있습니다. 아래는 물론 양방향 이겠죠?

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"
import { Question } from "./Question"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @ManyToMany(() => Question, (question) => question.categories)
    questions: Question[]
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany(() => Category, (category) => category.questions)
    @JoinTable()
    categories: Category[]
}

우리는 바루 만들어봤습니다. 참고할 점은 @JoinTable은 반대 관계는 갖기 않는다는 건데요. @JoinTable은 무조건 한쪽 방향에만 존재해야 합니다. 그리고 양방향 관계에서는 QueryBuilder를 사용해 양쪽에서 관계를 조인하게끔 가능합니다.

아래의 예시와 같습니다.

const categoriesWithQuestions = await dataSource
    .getRepository(Category)
    .createQueryBuilder("category")
    .leftJoinAndSelect("category.questions", "question")
    .getMany()

직접 프로퍼티(Property)를 만들어서 Many-to-many 관계 적용하는 방법

우리는 Many-to-many 관계에서 프로퍼티를 추가해야 할 필요가 있는데요, 우리는 새로운 엔티티를 만들어야 추가할 수 있습니다. 예를 들어서 Question이나 Category와 같은 엔티티가 있다고 가정하고 order 칼럼을 추가하려고 합니다. 그렇다면 우리는 QuestionToCategory라는 엔티티를 생성해야 합니다. 그리고 두 개의 ManyToOne데코레이션을 걸어주어서 관계를 맺어야 합니다.

아래의 예시와 같습니다.

import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from "typeorm"
import { Question } from "./question"
import { Category } from "./category"

@Entity()
export class QuestionToCategory {
    @PrimaryGeneratedColumn()
    public questionToCategoryId: number

    @Column()
    public questionId: number

    @Column()
    public categoryId: number

    @Column()
    public order: number

    @ManyToOne(() => Question, (question) => question.questionToCategories)
    public question: Question

    @ManyToOne(() => Category, (category) => category.questionToCategories)
    public category: Category
}

Question과 Category를 따르게 하여 관계를 추가할 수 있습니다. 아래 예시처럼 다시 oneToMany로 바꾸어야 하긴 합니다. 이래야 직접 추가가 가능합니다.

// category.ts
...
@OneToMany(() => questionToCategory, questionToCategory => questionToCategory.category)
public questionToCategories: QuestionToCategory[];

// question.ts
...
@OneToMany(() => QuestionToCategory, questionToCategory => questionToCategory.question)
public questionToCategories: QuestionToCategory[];

이상 끝.