Typeorm으로 Many-to-Many with custom property 구현하기

김가영·2021년 9월 22일
4

Node.js

목록 보기
33/34

시작하기

typeorm에서 Relation을 표현하는 방법은 기본적으로 굉장히 간단하다. @OneToMany, @ManyToMany 등 데코레이터를 이용하면 Relation이 생성되고 @JoinTable() 데코레이터를 이용하여 조인테이블을 한 번에 생성할 수도 있다.

@Entity()
export class Post {
    ...
    
    @ManyToMany(() => Category, category => category.posts)
    @JoinTable()
    categories: Category[];

}

@Entity()
export class Category {
    ...
    
    @ManyToMany(() => Post, post => post.categories)
    @JoinTable()
    posts: Post[];

}

문제는 join table에 원하는 property을 추가하고 싶을때 나타난다.
typeorm의 공식문서에서는 many-to-many relations with custom properties 라는 제목 아래 간단한 해결 방법을 설명하고 있다. one-to-many 를 두 번 이용하여 새로운 entity에 join해주는 것이 그 방법. 하지만 공식문서에서는 entity를 정의하는 것 외에 추가 설명이 부족하여 직접 구현한 내용을 정리해보고자 한다.

엔티티 생성하기

Post 는 여러개의 Category를 가질 수 있고, Category는 여러개의 Post를 가질 수 있다. -> Many-to-Many 관계이다.
원래대로라면 위의 예시처럼 간단하게 표현할 수 있지만 두 Post와 Category의 관계가 형성된 시간이 궁금하다고 가정해보자.

이를 위해서는 JoinTable에 createdAt 필드를 추가해야하므로 직접 join table의 엔티티를 생성하고 @OneTOMany()를 통해 Post와 Category에 각각 join해주는 방법을 이용해야 한다.

시작은 간단하게 JoinTable로 이용할 PostToCategory를 추가하는 것으로 시작한다.

import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Post } from "./post";
import { Category } from "./category";

@Entity()
export class PostToCategory {
    @PrimaryGeneratedColumn()
    public id!: number;

    @ManyToOne(() => Post, post => post.postToCategories)
    public post!: Post;

    @ManyToOne(() => Category, category => category.postToCategories)
    public category!: Category;

    @CreateDateColumn()
    public createdAt!: Date;

    @UpdateDateColumn()
    public updatedAt!: Date;
}

이제 이 PostToCategoryPostCategory에 연결해줘야한다. PostToCategory 엔티티를 간단하게 살펴보면, post와 category를 하나씩 연결하고 있으며, 해당 join이 발생한 시각을 createdAt 필드로 자동으로 저장할 것이다.

idcreatedAt, updatedAt 은 데코레이터를 통해 typeorm에서 자동으로 생성하도록 설정하였으므로 실제로 우리가 신경써줘야할 부분은postcategory 필드 뿐이다.

하나의 post가 여러개의 category와의 관계를 맺을 수 있으므로 여러개의 PostToCategory관계를 가질 수 있다. 하지만 하나의 PostToCategory는 하나의 post(그리고 하나의 category)와 관계를 맺을 수 있다. 그러므로 post -> postToCategory, category -> postToCategory는 oneToMany 관계이다.

PostCategory 엔티티에도 postToCategory 필드가 필요하다.

// category.ts
...
@OneToMany(() => PostToCategory, postToCategory => postToCategory.category)
public postToCategories!: PostToCategory[];

// post.ts
...
@OneToMany(() => PostToCategory, postToCategory => postToCategory.post)
public postToCategories!: PostToCategory[];

객체 생성하기

자 그럼 이제 category와 post 객체를 생성하고 연결해보자.
기본적으로 oneToMany 관계의 객체들을 저장할 때에는 두가지 방법을 이용할 수 있다.

// 방법1
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);
or alternatively you can do:

// 방법2
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);

방법1의 경우에는 many에 해당하는 객체들을 먼저 생성한 후 one에 해당하는 객체에 List로 넣어줬다.
방법2의 경우에는 one에 해당하는 객체를 먼저 생성한 후, many에 해당하는 객체들에 one을 연결해줬다.

하지만 현재 many에 해당하는PostToCategory 는 category와 post가 생성되기 전에 미리 생성될 수 없으므로 우리는 방법2만 이용할 수 있다.

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

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

const post1 = new Post();
await connection.manager.save(post1);

const postToCategory1 = new PostToCategory();
postToCategory.post = post1;
postToCategory.category = category1;
await connection.manager.save(postToCategory1);

const postToCategory2 = new PostToCategory();
postToCategory.post = post1;
postToCategory.category = category2;
await connection.manager.save(postToCategory2);

객체 읽어오기

객체를 읽어와보자.
Post와 Category는 직접 join되어있지 않기 때문에 객체를 읽어올 때에도 PostToCategory 를 이용하여 읽어와야한다.

const posts =  await connection
    .getRepository(Post) 
    .createQueryBuilder('post')
    .leftJoinAndSelect('post.postToCategories', 'postToCategories') // #1
    .leftJoinAndSelect('postToCategories.category', 'category') // #2
    .getMany()

post들과 연결된 category들을 함께 가져오는 query이다.
#1에서 각 post에 연결된 postToCategory들을 가져오고, #2에서 각 postToCategory에 연결된 category들을 가져오고 있다.

posts.map((post) => {
    return {
      ...post,
      categories: post.postToCategories ? post.postToCategories.map((item) => item.category) : [],
    };
  });

의 방법으로 post에 연관된 category들의 리스트인 categories 필드를 추가할 수 있다.

profile
개발블로그

0개의 댓글