본문 바로가기

Project/TypeORM

[번역] OneToMany / ManyToOne Relations

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

오늘은 목요일

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

Many-to-one과 One-to-many는 A엔티티는 B의 여러 인스턴스를 갖고 있는 관계입니다. 반대로 B는 A 엔티티의 하나의 인스턴스를 갖고 있습니다. 즉, A는 B의 여러 인스턴스 그리고 B는 오직 하나의 A 인스턴스를 갖고 있는 것을 의미합니다.

이제 늘 그렇듯 예제를 살펴보도록 하겠습니다. 예시는 A는 User Entity, B는 Photo Entity로 정의하고 시작하겠습니다. 다시 말씀드리자면, 위의 내용을 토대로 User는 여러개의 Photo를 가질 수 있지만, Photo는 단 하나의 User만 갖고 있는 것입니다.

Photo Entity

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User"

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

    @Column()
    url: string

    @ManyToOne(() => User, (user) => user.photos)
    user: User
}

User Entity

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"
import { Photo } from "./Photo"

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

    @Column()
    name: string

    @OneToMany(() => Photo, (photo) => photo.user)
    photos: Photo[]
}

위 예시를 보자면 User Entity에 @OneToMany를 추가했고 관계 타입을 명시해두었습니다. 

@OneToMany(() => Photo, (photo) => photo.user) 이 부분을 뜻하는 것입니다. 그리고 OneToOne과 달라진 부분은 우리는 @JoinColumn을 생략할 수 있다는 것입니다. 언제? OneToMany와 ManyToOne 관계에서 말이죠? 그리고 @OneToMany는 @ManyToOne없이는 존재할 수 없습니다. 만약 @OneToMany를 사용하고 싶다면, @ManyToOne은 필수요건중에 하나라고 말씀드릴 수 있습니다. 

그러나 반대는 그렇지 않습니다. 만약 @ManyToOne 관계를 갖고싶다면? 우리는 관계된 엔티티에 @OneToMany 없이 정의할 순 있습니다. 그리고 @ManyToOne을 지정해둔 Entity에 realation id와 외래키(FK)가 설정됩니다.

아래는 현재 테이블의 상태를 보여줍니다.

+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| url         | varchar(255) |                            |
| userId      | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

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

Photo에 @ManyToOne을 지정해놓았기 때문에 외래키가 설정된것이 보입니다. 

이제 여기서부터는 저장에 관해서 다루어볼 것입니다. 저장은 save 메소드를 사용합니다.

const photo1 = new Photo()
photo1.url = "me.jpg"
await dataSource.manager.save(photo1)

const photo2 = new Photo()
photo2.url = "me-and-bears.jpg"
await dataSource.manager.save(photo2)

const user = new User()
user.name = "John"
user.photos = [photo1, photo2]
await dataSource.manager.save(user)

또 다른 대안으로는 아래와 같은 코드 작성이 가능합니다.

const user = new User()
user.name = "Leo"
await dataSource.manager.save(user)

const photo1 = new Photo()
photo1.url = "me.jpg"
photo1.user = user
await dataSource.manager.save(photo1)

const photo2 = new Photo()
photo2.url = "me-and-bears.jpg"
photo2.user = user
await dataSource.manager.save(photo2)

Cascades를 설정해둠으로써, 우리는 단 한번의 save 를 사용해 이 관계를 저장할 수 있습니다. 제 생각입니다. OneToOne처럼 외래키가 존재하지 않는 것부터 저장을 한 후에는 자동으로 한번에 저장이 가능한 것을 의미하는 것 같습니다.

Find 옵션에 대해서 다루어보겠습니다.

Find 옵션에서는 관계를 분명히 명시해야합니다. 여러개 Photo와 함께 User를 불러오기 위해서는 말이에요.

const userRepository = dataSource.getRepository(User)
const users = await userRepository.find({
    relations: {
        photos: true,
    },
})

// or from inverse side

const photoRepository = dataSource.getRepository(Photo)
const photos = await photoRepository.find({
    relations: {
        user: true,
    },
})

또는 QueryBuilder를 사용해서 불러올 수도 있습니다.

const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .getMany()

// or from inverse side

const photos = await dataSource
    .getRepository(Photo)
    .createQueryBuilder("photo")
    .leftJoinAndSelect("photo.user", "user")
    .getMany()

여기서 주의해야될 점은, 관계에 eager loading을 설정해놓는다면, 우리는 관계를 불러올때 명시할 필요가 없이 항상 자동으로 같이 불러오게 될 것입니다. 근데 만약 우리가 QueryBuilder를 사용한다면 그렇지 않다는 것입니다. 뭐냐 그렇지 않냐면 자동으로 같이 불러오지 않기 때문에 무조건 비활성화 된다고 생각하시면 됩니다. 그래서 관계를 명시적으로 로드해야합니다.

즉 아래와 같이 정리할 수 있습니다.

Eager Loading: find 명령어를 사용할 때 관계를 자동으로 로드합니다. QueryBuilder에서는 비활성화됩니다.

QueryBuilder: 관계를 로드하려면 leftJoinAndSelect를 사용하여 명시적으로 관계를 지정해야 합니다.

이상 끝.