DB, ORM/🧊 typeORM

typeORM relationship 설정하기, (Many-to-One, One-to-One, many-to-many)

DarrenKwonDev 2020. 10. 26. 18:20

실습 세팅

 

관계형 DB가 그렇듯 다음과 같은 관계를 형성할 수 있다.

우선 실습을 위한 관계형 테이블을 만들기 위해 아래 툴을 이용해보자. 전반적인 감을 잡아보자.

 

dbdiagram.io/

 

이를 graphql에 반영하면 다음과 같다.

type User {
  id: Int!
  chat: Chat
  messages: [Message]
}

type Chat {
  id: Int!
  messages: [Message]!
  participants: [User]!
}

type Message {
  id: Int!
  text: String!
  chat: Chat!
  user: User!
}

 

 

이로써 작성해야 하는 관계는 다음과 같다.

1개의 Chat은 여러 개의 Message를 가진다.
=> Chat 테이블의 messages 컬럼은 OneToMany 관계로 Message 테이블과 관여
=> Message 테이블의 chat 컬럼은 ManyToOne 관계로 Chat 테이블과 관여

1개의 Chat은 여러 개의 User를 가진다.
=> Chat 테이블의 participants 컬럼은 OneToMany 관계로 User 테이블과 관여
=> User 테이블의 chat 컬럼은 ManyToOne 관계로 Chat 테이블과 관여

1개의 User는 여러 개의 Message를 가진다.
=> User 테이블의 messages 컬럼은 OneToMany 관계로 Message를 테이블과 관여
=> Message 테이블의 user 컬럼은 MaynToOne 관계로 User 테이블과 관여

 

Entity 작성

이제 이 graphql를 반영한 Entity를 작성해보자. 

 

User의 경우 다음과 같이 작성한다.

One(User)ToMany(Message). 다른 ORM도 그런데, 항상 앞에 오는 것이 자신, 뒤에 오는 것이 연결되는 테이블이다. 

chat 컬럼의 속성은 Chat이며 Chat 테이블 중 participants와 관여하는 것을 알 수 있다.

 

*) 너무 당연해서 생략하려고 했지만 당연히 messsage => message.user에서 message는 아무거나 써도 된다. Message Entity 중 특정한 한 개를 가리키는 인자일 뿐이므로.

import Chat from "./Chat";
import Message from "./Message";


@Entity()
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne((type) => Chat, (chat) => chat.participants)
  chat: Chat;

  @OneToMany((type) => Message, (message) => message.user)
  messages: Message[];
}

 

Chat의 Entity의 경우 다음과 같이 작성한다. 

import Message from "./Message";
import User from "./User";

@Entity()
class Chat extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @OneToMany((type) => Message, (message) => message.chat)
  messages: Message[];

  @OneToMany((type) => User, (user) => user.chat)
  participants: User[];
}

 

Message의 경우 다음과 같이 작성한다.

import Chat from "./Chat";
import User from "./User";

@Entity()
class Message extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: "text" })
  text: String;

  @ManyToOne((type) => Chat, (chat) => chat.messages)
  chat: Chat;

  @ManyToOne((type) => User, (user) => user.messages)
  user: User;
}

 

 

사용하실 때는 아래와 같이 relation이 설정된 column을 지정하여 찾아오시면 알아서 join이 됩니다.

const message = await Message.findOne({ id: args.userId }, { relations: ["text"] });

findOne의 경우 relations 외에도 cache, loadRelationIds 등도 자주 사용되므로 필요에 따라 적절하게 이용합시다.

export interface FindOneOptions<Entity = any> {
    /**
     * Specifies what columns should be retrieved.
     */
    select?: (keyof Entity)[];
    /**
     * Simple condition that should be applied to match entities.
     */
    where?: FindConditions<Entity>[] | FindConditions<Entity> | ObjectLiteral | string;
    /**
     * Indicates what relations of entity should be loaded (simplified left join form).
     */
    relations?: string[];
    /**
     * Specifies what relations should be loaded.
     */
    join?: JoinOptions;
    /**
     * Order, in which entities should be ordered.
     */
    order?: {
        [P in keyof Entity]?: "ASC" | "DESC" | 1 | -1;
    };
    /**
     * Enables or disables query result caching.
     */
    cache?: boolean | number | {
        id: any;
        milliseconds: number;
    };
    /**
     * Indicates what locking mode should be used.
     */
    lock?: {
        mode: "optimistic";
        version: number | Date;
    } | {
        mode: "pessimistic_read" | "pessimistic_write" | "dirty_read" | "pessimistic_partial_write" | "pessimistic_write_or_fail";
    };
    /**
     * Indicates if soft-deleted rows should be included in entity result.
     */
    withDeleted?: boolean;
    /**
     * If sets to true then loads all relation ids of the entity and maps them into relation values (not relation objects).
     * If array of strings is given then loads only relation ids of the given properties.
     */
    loadRelationIds?: boolean | {
        relations?: string[];
        disableMixedMap?: boolean;
    };
    /**
     * Indicates if eager relations should be loaded or not.
     * By default they are loaded when find methods are used.
     */
    loadEagerRelations?: boolean;
    /**
     * If this is set to true, SELECT query in a `find` method will be executed in a transaction.
     */
    transaction?: boolean;
}

 

 

 

조금 더 나아가서 join없이 연결된 테이블의 식별자만 확인할 수도 있습니다.

 

darrengwon.tistory.com/929?category=921419

 

조인 없이 관계 테이블 id 조회하기

조인 없이 그걸 어떻게 해? 라는 의문이 들지만 typeORM에서 구현해 놓았으므로 사용할 수 있습니다. 예를 들어 place라는 엔티티가 user와 relationship이 형성되어있다고 가정합시다. relationship이 설정

darrengwon.tistory.com

 

 

OneToOne 관계

ManyToOne이나 OneToMany는 위와 같이 작성하면 되지만 OneToOne은 조금 다릅니다.

 

Ride.ts 엔티티

Chat과 연결되어 있으며 chat 엔티티의 ride라는 컬럼을 통해 연결되어 있음을 나타냄

@OneToOne((type) => Chat, (chat) => chat.ride, { nullable: true })
@JoinColumn()
chat: Chat;

Chat.ts 엔티티

Ride와 연결되어 있으며 ride 엔티티의 chat이라는 컬럼을 통해 연결되어 있음.

@OneToOne((type) => Ride, (ride) => ride.chat)
ride: Ride;

 

보시다시피 OneToOne을 사용하면 됩니다. 그런데 @JoinColumn이라는 데코레이션이 붙었습니다.

설명을 읽어보자면

 

We also added @JoinColumn which is required and must be set only on one side of the relation.(1:1 관계 테이블에서 한 쪽에만 설정해야 합니다) The side you set @JoinColumn on, that side's table will contain a "relation id" and foreign keys to target entity table

 

1:1 관계에서 realtion id와 fk를 가져야 할 쪽, 쉽게 말하자면 1:1 관계의 주인 쪽에 @JoinColumn을 붙여주면 됩니다. 

이걸 결정하기 위해서는 어느 쪽을 통해 다른 쪽의 정보를 받아올 것인지를 생각해보면 됩니다.

 

공식 문서에서는 User와 Profile과의 관계를 들어 설명하고 있습니다. User를 통해서 profile을 불러와야하는 식으로 사용해야 하죠. 그러면 User에 @JoinColumn을 붙이면 됩니다. 실사용은 다음과 같이 profile을 가져올테니까요.

const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["profile"] });

 

왜 다른 관계에서는 @JoinColumn을 넣어주지 않아도 되는가...하면 공식 문서에 다음과 나와 있습니다.
You can omit @JoinColumn in a @ManyToOne / @OneToMany relation. @OneToMany cannot exist without @ManyToOne. If you want to use @OneToMany, @ManyToOne is required. Where you set @ManyToOne - its related entity will have "relation id" and foreign key.

 

ManyToMany 관계

typeorm.io/#/many-to-many-relations

 

한 카테고리는 여러 Question을 가질 수 있고

한 Question은 여러 카테고리를 가질 수 있습니다.

 

보면 알겠지만, 특정 필드와 연결시키지는 않는다. 편하다 ㅎ

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

@Entity()
export class Category {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

}
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[];

}

 

OneToOne과 마찬가지로 @JoinTable()이 필요합니다.

You must put @JoinTable on one (owning) side of relation.

 

아시다시피 many-to-many 관계는 중간 테이블이 하나 생성됩니다.

+-------------+--------------+----------------------------+
|                        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    |
+-------------+--------------+----------------------------+