TypeORM Relations

2020. 10. 4. 14:05DEV/TypeORM

반응형

Relations

관계형 Entity 설계

  • 1:1 : @OneToOne
  • N:1 : @ManyToOne
  • 1:N : @OneToMany
  • N:N : @ManyToMany

One-To-One

서로 1:1로만 매칭이 되는 모델에 사용

관계 설정

@OneToOne

  • Target Type을 지정하여 연결 설정
  • 단방향/양방향 설정 가능
    • 단방향 : 한쪽에만 설정
    • 양방향 : 양쪽에 설정

@JoinColumn

  • 관계 중 Foreign Key가 있는 쪽에 설정 필수
  • 설정된 테이블에 "relation id"와 foreign key가 저장됨
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(type => Profile)
    @JoinColumn()
    profile: Profile;

}

생성된 데이터

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

호출 방법

Save

한 번만 호출해서 저장 가능

const profile = new Profile();
profile.gender = "male";
profile.photo = "me.jpg";
await connection.manager.save(profile);

const user = new User();
user.name = 'Joe Smith';
user.profile = profile;
await connection.manager.save(user);

find

정보를 join해서 호출

특정 FindOptions에 특정 관계 설정 필요

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

또는, QueryBuilder로 Join

const users = await connection
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.profile", "profile")
    .getMany();

Many-To-One / One-To-Many

  • 1:N, N:1 등의 관계 설정
  • @OneToMany와 @ManyToOne은 반드시 함께 설정
  • @JoinColumn 생략 가능 > @ManyToOne에 Foriegn Key 생성

설정 방법

예를 들어 User(1): Photo(N)을 설정

  • User : @OneToMany
  • Photo : @ManyToOne
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne} from "typeorm";
import {User} from "./User";

@Entity()
export class Photo {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    url: string;

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

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

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

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

}

생성된 데이터

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

사용 방법

save

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

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

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

또는

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

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

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

find

@findOptions로 관계호출

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

// or from inverse side

const photoRepository = connection.getRepository(Photo);
const photos = await photoRepository.find({ relations: ["user"] });

QueryBuilder로 Join

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

// or from inverse side

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

Many-To-Many [WIP]

서로가 N:N으로 연결되는 Category와 Question의 경우

관계 설정

@ManyToMany

  • Owining side에 설정

@JoinTable

  • @ManyToMany 필수
  • Owining side에 설정
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(type => Category)
    @JoinTable()
    categories: Category[];

}

생성된 데이터

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

사용 방법 [WIP]

save

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

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

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

delete realationship

  • 관계를 삭제한 후 저장하는 방식
  • JoinTable의 관계만 제거 되고 각 테이블의 데이터는 보존됨
const question = getRepository(Question);
question.categories = question.categories.filter(category => {
    category.id !== categoryToRemove.id
})
await connection.manager.save(question)

Relation Options

  • eager: boolean - If set to true, the relation will always be loaded with the main entity when using find* methods or QueryBuilder on this entity
  • cascade: boolean | ("insert" | "update")[] - If set to true, the related object will be inserted and updated in the database. You can also specify an array of cascade options.
  • onDelete: "RESTRICT"|"CASCADE"|"SET NULL" - specifies how foreign key should behave when referenced object is deleted
  • primary: boolean - Indicates whether this relation's column will be a primary column or not.
  • nullable: boolean - Indicates whether this relation's column is nullable or not. By default it is nullable.

Cascade

  • cascade 설정하면 둘 사이 영속 관계가 설정됨
  • eg. One Side 삭제 시 Many Side도 삭제됨
@Entity()
class Book extends BaseEntity {
    @ManyToOne(() => Author, (author) => author.books, {
        onDelete: 'CASCADE',
    })
    public author?: Author
}

@Entity()
class Author extends BaseEntity {
    @OneToMany(() => Book, (book) => book.author, {
        cascade: true,
    })
    public books: Book[];
}
  • Author 삭제 시 Book도 삭제됨
  • onDelete sets the authorId foreign key to CASCADE onDelete on Book. This means that when the author is deleted, the book is also deleted. >> REF

@JoinColumn & Unique Key

  /**
   * Project의 Target Lang에 따른 Task 생성 관계 설정
   * - TaskType: Translation
   */
  @Column({ nullable: true })
  projectId?: number

  @ManyToOne((type) => ProjectEntity, (project) => project.tasks, {
    onDelete: 'CASCADE',
  })
  /**
   * @param name : 현재 Entity의 컬럼명
   * @param referencedColumnName : Join 대상 Entity의 컬럼명
   */
  @JoinColumn({ name: 'projectId', referencedColumnName: 'id' })
  project?: ProjectEntity
  ---
  /**
   * Task Join 설정
   * - Task.targetId = Project.id and Task.type = TaskType.TRANSLATION
   */
  @OneToMany((type) => TaskEntity, (task) => task.project, {
    cascade: true,
  })
  tasks: TaskEntity[]

References

🔗 Multiple JOIN with TYPEORM

🔗 주니어개발자 앱개발기3 - TypeORM Relations, migration errors

🔗 TypeORM 시작하기 (4) - 관계

반응형

'DEV > TypeORM' 카테고리의 다른 글

TypeORM Migration하기  (0) 2020.10.06
TypeORM Entity  (0) 2020.10.03
TypeORM Pattern  (0) 2020.10.02