kakasoo

TypeORM Repository와 ManyToMany 본문

프로그래밍/NestJS

TypeORM Repository와 ManyToMany

카카수(kakasoo) 2022. 7. 17. 12:55
반응형

Entity 정의

상품의 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에도 접근 가능합니다.

 

OneToMany를 이용한 GET

@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을 이용하면 더 간단히 구현이 가능합니다.

물론 이를 위해서는 미리 관계를 설정할 필요가 있습니다.

 

ManyToMany 설정하기

@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가 별 것도 아닌데, 처음 할 때는 상당히 곤혹스럽다는 걸 잘 알아서요.

언제든지 질문 + 피드백 주세요.

 

22.07.17

이제는 TypeORM 0.3.x 버전을 쓰면서 코드를 작성하는 방식이 달라졌습니다.

하지만 저 코드도 아직 deprecated 된 것은 아니므로 여전히 사용할 수 있습니다.

반응형