웹/Nest

[Nestjs] @ManyToMany를 OneToMany2개로 쪼개고 select 하기

wonin 2023. 7. 30. 08:30

ManyToMany를 왜 쓰지 말라고 할까요? 

제가 생각하는 이유는 확장성 때문이라고 생각합니다. 중간 테이블의 역할이 PK, FK 쌍을 알아서 매핑만 하는 역할이면 ManyToMany를 써도 상관없다고 생각합니다. 하지만 중간 테이블에 어떤 column이 추가가 되거나 삭제가 되야한다면 문제가 될 수 있기 때문 아닐까요?

 

그래서 테이블을 엮는 2가지 방법을 소개해 드립니다. 1번은 ManyToMany를 사용해서 select을 하고요.  2번은 OneToMany2개를 사용해서 select을 합니다.

 

 

A, B 테이블이 있습니다. 이 2개의 테이블은 서로 다대다 관계입니다. 

🟩 1. ManyToMany를 사용해서 관계맺기

A

@Entity({ schema: 'sleact' })
export class A {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => B, (b) => b.As)
  @JoinTable({
    name: 'ab',
    joinColumn: {
      name: 'AId',
      referencedColumnName: 'id',
    },
    inverseJoinColumn: {
      name: 'BId',
      referencedColumnName: 'id',
    },
  })
  Bs: B[];
}

A클래스에서 ManyToMany와 OneToMany를 2개다 연결해놨습니다.

ManyToMany에서 JoinTable에 joinColumn과 inverseJoinColumn을 넣어두면 table이 생성됩니다.

name을 적지 않으면 typeorm이 정해주는 기본 이름으로 table이 생성됩니다.

 

B

@Entity({ schema: 'sleact' })
export class B {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => A, (a) => a.Bs)
  As: A[];
}

[검색하기]

A를 검색할 때 B를 배열형태로 받고싶다면

return await this.aRepository.find({
      where: { name: name },
      relations: {
        Bs: true,
      },
    });

이런 형태로 가지면 되었습니다. 간단하죠

A를 검색하면 B에 어떤것이 있는지 딸려서 옵니다. 간단하게 할 수 있습니다.

 

 

🟩 2. ManyToMany를 OneToMany2개로 쪼개기

@OneToMany관계설정하고 join 테이블에 2개로 쪼개진 객체를 조회하는 방법이 있습니다.

(OneToMany ↔ ManyToOne2 ↔ OneToMany)

A ↔ C ↔ B 테이블 관계로 설명하겠습니다.

 

A

@Entity({ schema: 'sleact' })
export class A {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => C, (c) => c.As)
  Cs: C[];
}

 

C

@Entity({ schema: 'sleact' })
export class C {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => A, (a) => a.Cs)
  As: A[];

  @ManyToOne(() => B, (b) => b.Cs)
  Bs: B[];
}

C라는 중간 Table을 수동으로 만들어줘야 합니다. A, B의 id에 맞게 관계도 설정해야 합니다.

❗ 여기서 ManyToMany의 단점이 나옵니다.

예를들어 C테이블에 A,B에서 들어온 값의 합 count column을 넣어야 하는 상황일 때 혹은 생성시간을 넣어야 하는 상황이면?ManyToMany에서 만들어진 테이블을 컨트롤할 수 없기 때문에 불가능합니다. 그래서 OneToMany 2개로 쪼갠 이유는 C 테이블에 커스텀을 하기 위해서 입니다. 

 

B

@Entity({ schema: 'sleact' })
export class B {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => C, (c) => c.Bs)
  Cs: C[];
}

[검색하기]

    const fullData = await this.aRepository.find({
      where: { name: name },
      relations: {
        Cs: {
          Bs: true,
        },
      },
    });

문제는 A를 검색할 때 B를 같이 검색하는것에 문제가 생겼습니다.

C의 값하고 B의 값이 같이 나오는것이였습니다.

Cs를 거치고 그 안에 Bs가 어떤것이 있는지 보여주게 됩니다. 불필요한 C의 값과 B의 값이 나오게 되고 depth가 늘어나게 됐습니다.

저는 위의 ManyToMany에서 썻던것 처럼 B의 값만 보려고 합니다.

 

제가 찾은 방법은 저 결과를 조회하고 filltering하는 방법입니다.

  async findAll(name) {
    const fullData = await this.aRepository.find({
      where: { name: name },
      relations: {
        Cs: {
          Bs: true,
        },
      },
    });


    const filteredData = fullData.map((item) => {
      // Create a copy of the item object without the 'Cs' property
      const { Cs, ...itemWithoutCs } = item;
      return {
        // Spread the original item data without 'Cs'
        ...itemWithoutCs,
        // Replace Cs with just the Bs data
        Bs: item.Cs.map((csItem) => csItem.Bs), // Change 'Cs' to 'Bs' here
      };
    });
    return filteredData;
  }

ManyToMany와 결과값이 비슷해졌습니다.

 

 


 

🟩 3. a를 삭제하면 c의 값이 삭제되었으면 좋겠습니다. 

@Entity({ schema: 'sleact' })
export class C {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => A, (a) => a.Cs, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
  As: A[];

  @ManyToOne(() => B, (b) => b.Cs, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
  Bs: B[];
}

C테이블의 연관값에 cascade를 걸어야 합니다.

A테이블, B테이블의 OneToMany에 걸면 casecade가 적용이 안됩니다. 왜냐하면 칼럼이 없기 때문입니다. ManyToOne이 있는 곳에서 cascade를 걸어서 A, B가 수정, 삭제되었을 때 같이 수정되도록 할 수 있습니다.

728x90