상품의 Entity 정의
/**
* 상품의 Entity
*/
@Entity('product', { schema: 'db_name' })
export class Product {
@PrimaryGeneratedColumn({ type: 'int', name: 'idx' })
idx: number;
@Column('varchar', { name: 'title', length: 255 })
title: string;
}
해시태그의 Entity 정의
/**
* 해시태그의 Entity
*/
@Entity('hashtag', { schema: 'db_name' })
export class Hashtag {
@PrimaryGeneratedColumn({ type: 'int', name: 'idx' })
idx: number;
@Column('varchar', { name: 'hashtag', unique: true })
text: string;
}
hashtag도 index 번호와 text만 가지게끔 간단하게 정의가 됐습니다.
이런 다대 다 관계에서, 우리는 중간에 1:n, m:1이 되도록 관계 테이블을 만들 수 있을 거에요.
상품과 해시 테이블 간 관계 Entity 정의
@Index('fk_product_has_hashtag_productId', ['productId'])
@Index('fk_product_has_hashtag_hashtagId', ['hashtagId'])
@Entity('product_has_hashtag', { schema: 'db_name' })
export class ProductHasHashtag {
@PrimaryGeneratedColumn({ type: 'int', name: 'idx' })
idx: number;
@Column('int', { name: 'product_id' })
productId: number;
@Column('int', { name: 'hashtag_id' })
hashtagId: number;
@ManyToOne(() => Product, (product) => product.idx)
@JoinColumn([{ name: 'product_id', referencedColumnName: 'idx' }])
product: Product;
@ManyToOne(() => Hashtag, (hashtag) => hashtag.idx)
@JoinColumn([{ name: 'hashtag_id', referencedColumnName: 'idx' }])
hashtag: Hashtag;
}
여기까지 정의가 되었다면 이제 상품과 해시태그의 관계도 정의가 된 셈입니다.
TypeORM에서는 한 쪽에서만 관계를 지정해도 다른 한 쪽은 굳이 할 필요 없으니깐요.
하지만 보통 관계 테이블에서부터 시작하는 경우는 없으니, 반대쪽에서 OneToMany를 지정해줍시다.
/**
* 상품의 Entity
*/
@Entity('product', { schema: 'db_name' })
export class Product {
@PrimaryGeneratedColumn({ type: 'int', name: 'idx' })
idx: number;
@Column('varchar', { name: 'title', length: 255 })
title: string;
@OneToMany(() => ProductHasHashtag, relation => relation.product)
productHasHashtags : ProductHasHashtag[];
}
이제 상품에서 productHasHashtag에 접근 가능하고, 이를 통해 hashtag에도 접근 가능합니다.
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productsRepository: Repository<Product>
){}
async getProductWithHashtag(idx) {
// 어떻게 접근해야 하는가?
}
}
하지만 이 단계에서는 Repository Pattern을 이용해서 hashtag와 함께 상품을 조회할 수 없습니다.
분명 상품이 해시태그들을 갖는 게 맞음에도 불구하고 중간에 관계 테이블을 거쳐야 합니다.
그래서 코드는 상당히 더러워질 수 밖에 없죠.
async getProductWithHashtag(idx) {
return await this.productsRepository.find({
relation : ['productHasHashtags'], // OneToMany가 정의되었기에 가능하다.
where : { idx }
})
}
이렇게 하면 이제 상품 Entity에 productHasHashtag라고 하는 관계 테이블을 Left Join해서 가져올 수 있고,
async getProductWithHashtag(idx) {
return await this.productsRepository.find({
relation : ['productHasHashtags', 'productHasHashtags.hashtag'],
where : { idx }
})
}
이제 상품에서 해시태그까지 접근이 가능해집니다.
하지만 돌아오는 return 값이 상당히 더러운 걸 볼 수 있을텐데요, 이는 product에서 hashtag로 바로 접근한 게 아니고, 관계 테이블을 하나 거치기 때문입니다.
우리가 원하는 건 product 라는 객체에 hashtag 라는 키 값으로 배열을 가지는 구조일 것입니다.
이를 고치기 위해서는 query를 날리거나 QueryBuilder를 사용할 수 있겠습니다만,
Repository Pettern을 이용하면 더 간단히 구현이 가능합니다.
물론 이를 위해서는 미리 관계를 설정할 필요가 있습니다.
@Entity('product', { schema: 'db_name' })
export class Product {
@PrimaryGeneratedColumn({ type: 'int', name: 'idx' })
idx: number;
@Column('varchar', { name: 'title', length: 255 })
title: string;
// @OneToMany(() => ProductHasHashtag, relation => relation.product)
// productHasHashtags : ProductHasHashtag[];
@ManyToMany(() => Hashtag, (hashtag) => hashtag.products)
@JoinTable({
name: 'product_has_hashtag',
joinColumn: { name: 'product_id', referencedColumnName: 'idx' },
inverseJoinColumn: { name: 'hashtag_id', referencedColumnName: 'idx' },
})
hashtags: Hashtag[];
}
관계 테이블을 거치지 않고 Product에서 Hashtag를 바로 참조할 수 있게 정의했습니다.
ManyToMany로 관계를 명시해준 다음에,
두 테이블이 어떻게 관계를 맺었는가를 명시해주기 위해 JoinTable을 사용합니다.
JoinTable 안에서는 name 값으로 관계 테이블의 이름을 명시해줍니다.
저는 이를 product_has_hashtag라고 했는데, 이는 다시 위로 올라가 Entity를 정의한 부분을 참고하시면 됩니다.
joinColumn은 상품 입장에서, 상품이 어떻게 참조되어 있는지를 말하는 것이고,
inverseJoinColumn은 역으로 참조가 어떻게 일어났는지, 즉 hashtag의 입장을 말합니다.
두 측에 대한 정의가 모두 끝났으면, 이제 Service 로직에서 다르게 접근이 가능합니다.
async getProductWithHashtag(idx) {
return await this.productsRepository.find({
relation : ['hashtags'],
where : { idx }
})
}
서비스에 이 함수를 구현한다면 이제 정상적으로 동작하는 것을 확인할 수 있습니다.
안타깝게도, 제가 이 코드를 직접 테스트한 것은 아닙니다.
회사 코드를 올릴 수는 없어서, 생각나는대로 타이핑을 하다보니 오탈자가 있을 수 있습니다.
Nest.js와 TypeORM을 공부하시는 분이라면, 언제든 기탄없이 질문해주셔도 좋습니다. :)
ManyToMany가 별 것도 아닌데, 처음 할 때는 상당히 곤혹스럽다는 걸 잘 알아서요.
언제든지 질문 + 피드백 주세요.
@ManyToMany 데코레이터를 Product 테이블에만 쓰셨는데, Hashtag 테이블에도 설정해줘야하는거죠?
@JoinTable 데코레이터는 둘중 한곳만 써도된다고 알고있어요