릴레이션

ClassBinu·2024년 5월 20일

F-lab

목록 보기
27/65

릴레이션?

릴레이션으로 관련 엔티티의 연결을 쉽게 할 수 있다.

  • @OneToOne
  • @ManyToOne
  • @OneToMany
  • @ManyToMany

cascade

cascade가 true이면 save를 호출하지 않아도 관련 객체가 저장되므로 주의

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

    @Column()
    title: string

    @Column()
    text: string

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

@JoinColumn

다른 열에 대한 참조(외래키를 사용)
기본적으로 관련 엔터티의 기본 열을 참조함.

@JoinTable

many-to-many 접합 테이블 조인 열을 설명함.
접합 테이블: TypeORM에서 자동으로 생성된 특별한 별도 테이블

OneToOne

A가 오직 하나의 B 인스턴스를 포함하고,
B오 오직 하나의 A 인스턴스를 포함하는 관계

대표적으로 User와 Profile 관계

단방향 관계 예시

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

    @Column()
    gender: string

    @Column()
    photo: string
}

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

    @Column()
    name: string

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

여기서는 user가 owerner side인데 profile이 owener side인 것이 더 확장성 있지 않을까..? 생각함.

불러오기

OneToOne 관계는 이렇게 불러올 수 있음.

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

위 ORM은 다음 형식으로 SQL 형태로 변환될 것으로 추정

SELECT u.*, p.*
FROM User u
LEFT JOIN Profile p ON u.profileId = p.id

단방향은 한쪽에만 관계 데코레이터가 있는 관계
양방향은 관계의 양쪽에 있는 데코레이터와의 관계

양방향 관계 예시

@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
}

@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
}

어떤 Profile 엔티티의 속성(user)이 이 관계에 연결되는지를 나타냅니다. 즉, 각 Profile은 하나의 User와 연결됩니다.

두 번째 매개변수는 "역방향(referencing) 관계"를 정의하는 함수. 이 매개변수는 관계의 "주인(owner)" 쪽이 아닌 다른 쪽에서 이 관계를 어떻게 참조하는지를 설명합니다.

Many to One / One To Many

A가 B의 여러 인스턴스를 포함
하지만 B는 A 인스턴스 하나만 포함
예를 들어 User와 Photo

User 인스턴스는 여러 Photo 인스턴스를 가질 수 있지만,
Photo 인스턴스는 하나의 User 인스턴스만 가질 수 있다.

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

    @Column()
    url: string

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

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

    @Column()
    name: string

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

@OneToMany cannot exist without @ManyToOne.
If you only care about the @ManyToOne relationship, you can define it without having @OneToMany on the related entity.

Many to Many

A인스턴스가 다수의 B인스턴스를 포함할 수 있고,
B인스턴스도 다수의 A인스턴스를 포함할 수 있는 관계

대표적으로 질문과 카테고리를 들 수 있음.
하나의 질문은 다수의 카테고리를 가질 수 있고,
하나의 카테고리는 다수의 질문을 가질 수 있음.

이건 단방향 관계

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

    @Column()
    name: string
}

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

    @Column()
    title: string

    @Column()
    text: string

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

양방향 관계

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

    @Column()
    name: string

    @ManyToMany(() => Question, (question) => question.categories)
    questions: Question[]
}

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

    @Column()
    title: string

    @Column()
    text: string

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

Eager and Lazy Relations

// eager
 @ManyToMany((type) => Question, (question) => question.categories)
 questions: Question[]

// lazy
@ManyToMany((type) => Question, (question) => question.categories)
questions: Promise<Question[]>

lazy는 Question 엔티티와의 관계가 실제로 필요할 때까지 데이터베이스에서 로딩을 지연시키고, 해당 데이터에 접근하는 순간 데이터를 가져오는 구조입니다.

레이지 로딩의 단점
N+1 문제:

레이지 로딩을 사용하면 각 엔티티에 대해 별도의 쿼리가 발생할 수 있습니다. 예를 들어, 여러 Category에 속하는 각각의 Question을 로딩할 때, 각 Question에 대해 별도의 데이터베이스 쿼리가 실행될 수 있으며, 이는 성능 저하를 초래할 수 있습니다.

JS you have to use promises if you want to have lazy-loaded relations. This is non-standard technique and considered experimental in TypeORM.

N+1 문제

N+1 문제는 하나의 데이터를 가져온 후, 관련된 각 데이터를 개별적으로 추가로 조회할 때 발생
한 번의 쿼리로 해결 가능한 쿼리를 N+1번의 쿼리 발생으로 인한 성능 저하 문제를 말함.
(배치를 쓰면 2번 정도에 끝남..?)

데이터베이스에 1개의 Category와 이 카테고리에 속하는 N개의 Question이 있다고 가정
1. 첫 번째 쿼리는 Category를 가져오는 쿼리
1. 각 Category에 속하는 Question을 가져오기 위한 추가 쿼리들: 만약 데이터베이스에 10개의 Category가 있다면, 각 Category에 대해 연결된 Question을 가져오기 위해 추가적인 쿼리가 필요합니다. 각 Category당 하나의 쿼리가 실행되므로, 이는 10번의 추가 쿼리를 발생

N+1 문제의 해결 방법

  • 조인 사용: SQL 조인을 사용하여 필요한 모든 데이터를 하나의 쿼리로 가져올 수 있습니다. 이 방법은 여러 관계를 한 번에 로딩하여 추가 쿼리를 줄입니다.
  • 배치 처리: ORM 기능을 사용하여 관련된 데이터를 한 번에 묶어서 요청하는 방법입니다. 예를 들어, - - - TypeORM의 find 메소드에서 relations 옵션을 통해 필요한 관계를 미리 지정할 수 있습니다.
    데이터 페칭 전략 변경: 레이지 로딩 대신 이질릭 로딩(eager loading)을 사용하여 관련 데이터를 처음부터 함께 로드할 수 있습니다.

FAQ

객체를 직접 넣지 않고 fk만 넣을 수도 있음.

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

    @Column()
    name: string

    @Column({ nullable: true })
    profileId: number

    @OneToOne((type) => Profile)
    @JoinColumn() // 외래 키 컬럼 이름을 자동으로 'profileId'로 지정
    profile: Profile
}

나의 오해

관계를 설정하면 해당 필드에 외래키가 들어가는 줄 알았음.
근데 그게 아니라 이건 일종의 참조 필드로 실제 데이터베이스 테이블에 명시되는 건 아니였음.
만약 외래키를 표시하고 싶다면 별도의 Id 필드를 만들어 줘야 함.

@OneToOne 같은 관계 설정 데코레이터와 @JoinColumn을 사용하는 것은 객체 간의 관계를 정의하는 방법이다!

참조 필드와 외래키 필드 구분하기

0개의 댓글