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;
}
이제 이 PostToCategory
를 Post
와 Category
에 연결해줘야한다. PostToCategory
엔티티를 간단하게 살펴보면, post와 category를 하나씩 연결하고 있으며, 해당 join이 발생한 시각을 createdAt
필드로 자동으로 저장할 것이다.
id
와 createdAt
, updatedAt
은 데코레이터를 통해 typeorm에서 자동으로 생성하도록 설정하였으므로 실제로 우리가 신경써줘야할 부분은post
와 category
필드 뿐이다.
하나의 post가 여러개의 category와의 관계를 맺을 수 있으므로 여러개의 PostToCategory관계를 가질 수 있다. 하지만 하나의 PostToCategory는 하나의 post(그리고 하나의 category)와 관계를 맺을 수 있다. 그러므로 post -> postToCategory, category -> postToCategory는 oneToMany 관계이다.
Post
와 Category
엔티티에도 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
필드를 추가할 수 있다.