본문 바로가기

Project/TypeORM

[번역] @OneToOne Relations

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

목요일

일단 현재 A Entity가 있고 B Entity가 있다고 가정하고 시작해보겠습니다.

One-to-one에서 A는 B의 딱 한 개의 인스턴스를 갖고 있습니다. B 또한 마찬가지로 딱 한개의 A의 인스턴스를 갖고 있는 관계를 의미합니다. 이제 A라고 부르지말고 A는 User Entity이고, B는 Profile Entity라고 정의하고 시작해 보겠습니다.

User는 한개의 Profile을 갖고 있고, Profile 또한 오직 한 명의 User를 갖고 있다.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

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

    @Column()
    gender: string

    @Column()
    photo: string
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    OneToOne,
    JoinColumn,
} from "typeorm"
import { Profile } from "./Profile"

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

    @Column()
    name: string

    @OneToOne(() => Profile)
    @JoinColumn()
    profile: Profile
}

위 코드를 보자면, User에 @OneToOne 데코레이터를 작성했습니다. 그리고 Profile을 가리키는 것을 명시해 두었습니다. 그리고 @JoinColumn을 작성된 것을 볼 수 있는데요. 이 @JoinColumn은 무조건 한쪽에만 작성해주어야 합니다. 그리고 이를 포함한 엔티티는 relation id와 외래키(FK)가 포함될 것입니다.

작성된 테이블의 결과는 이렇습니다.

+-------------+--------------+----------------------------+
|                        profile                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| gender      | varchar(255) |                            |
| photo       | varchar(255) |                            |
+-------------+--------------+----------------------------+

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

자 다시 말씀드리지만, @JoinColumn은 무조건 한쪽 테이블에만 작성해야 합니다. 그래야 한쪽에만 외래키를 넣어줄 수 있습니다.

이어서 "save" 메서드에 대해서 설명드리겠습니다. 우리가 TypeORM을 사용하는 이유는 좀 더 효율적으로 코드를 작성하고 최적화된 쿼리를 만들기 위해서이지 않습니까?

우리는 "save" 메서드를 사용해 데이터베이스에 저장할 때요. @JoinColumn이 없는 것부터 저장을 해야 합니다. 왜냐하면 외래키를 연결해야 하는데, 외래키가 아직 생성되지 않고 User를 먼저 저장을 해준다면? 현재 User는 외래키가 존재하지 않은 상태가 되는 것입니다.

그래서 Profile 먼저 저장하고 User를 저장해 한번만 저장을 해주면 되는데! 반대로 진행되게 된다면 Update문을 통해서 User를 한번 더 쿼리를 작성해야 하는 문제가 발생하게 됩니다. 그렇다면 우리가 원했던 방식이 옳지 않게 되겠죠? 최적화된 쿼리로 성능을 높여야 하는데 말이죠? 이렇기 때문에, 항상 JoinColumn이 없는 것부터 저장해 외래키를 확보해 두어야 합니다. 혹은 다른 경우도 발생할 수 있겠죠? Profile이 없는 경우도 있을 테지만, 보통 회원가입 시 정보도 먼저 작성되기 때문에 이러한 경우에는 위의 규칙을 따라야 최적화시킬 수 있다는 말입니다.

Find 옵션입니다.

Find Options을 사용해 엔티티를 조회할 때, relations 옵션을 사용해 연관된 엔티티를 전부 가져올 수 있다는 뜻입니다.

const users = await dataSource.getRepository(User).find({
    relations: {
        profile: true,
    },
})
const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.profile", "profile")
    .getMany()

또는 쿼리 빌더를 사용해서 조인을 할 수도 있습니다.

Eager Loading(즉시 로딩)입니다.

Eager loading이 활성화된 경우, find 메서드에서 relations를 지정하지 않더라도, 자동으로 항상 로드가 되는데요. 하지만 QueryBuilder는 eager loading을 비활성화합니다. 즉, QueryBuilder를 사용할 때는 관계를 꼭 지정해주어야 합니다.

Eager Loading은 엔티티를 조회할 때 연관된 엔티티를 자동으로 함께 로드하는 것을 의미합니다. 즉, 특정 엔티티를 조회할 때, 이 엔티티와 연관된 다른 엔티티들도 자동으로 데이터베이스에서 가져오게 됩니다.

더보기

Eager Loading의 

장점은 사용이 간편하고 관계된 엔티티를 명시적으로 로드하지 않아도 자동으로 로드가 된다는 점입니다. 그리고 코드가 간결해지고 관계된 데이터를 로드하기 위해 따로 옵션을 추가할 필요가 없다는 것입니다.

단점은 모든 조회 시 관계된 엔티티를 항상 로드하므로, 성능에 영향을 미칠 수 있습니다. 예를 들어 필요한 경우가 아닌데도 관계된 엔티티가 항상 로드되기 때문에, 불필요한 데이터 조회로 인해 쿼리가 느려지는 경우가 발생합니다.

특히나 대규모의 데이터나 많은 관계를 가진 엔티티를 다룰 때는 필요하지 않은 데이터를 불필요하게 로드하지 않도록 eager loading을 피하고, 필요한 경우에만 명시적으로 관계를 로드합시다.

 

양방향 설정은 어떻게 해?

현재는 단방향으로 설정되어 있습니다. 이어서 관계 설정은 단방향과 양방향을 할 수 도 있습니다. 단방향은 오직 한쪽 테이블에서만 @OneToOne을 추가해 주면 됩니다. 양방향은 양쪽 테이블에 추가해주면 됩니다. 우리는 이 내용을 코드로 작성하다면 이렇습니다.

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

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

    @Column()
    gender: string

    @Column()
    photo: string

    @OneToOne(() => User, (user) => user.profile) // specify inverse side as a second parameter
    user: User
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    OneToOne,
    JoinColumn,
} from "typeorm"
import { Profile } from "./Profile"

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

    @Column()
    name: string

    @OneToOne(() => Profile, (profile) => profile.user) // specify inverse side as a second parameter
    @JoinColumn()
    profile: Profile
}

우리는 우리의 양방향 관계를 만들었습니다. 하지면 반대편은 @JoinColumn이 존재하지 않죠? 이는 한쪽 관계에만 존재해야 하고, 외래키를 소유할 테이블에만 존재합니다. 이것에 대한 이유는 해당 페이지에 이유가 존재합니다. 

ctrl + f  + < Product Entity >를 찾아 더 보기를 찾아주세요.

이렇게 번역을 통해서 이해를 하는 것이 좋다고 판단됩니다. 왜냐하면, 그래도 오피셜인데 거짓된 정보는 없지 않을까 생각됩니다. 이상 OneToOne에 관한 번역을 했습니다. 계속해서 필요한 내용은 번역을 하겠습니다.