-
[TypeORM] Relation 관계에서 Join을 하는 방법들Javascript, Typescript 2021. 12. 8. 22:27반응형
2022.09.28
TypeORM 2.41 버전을 기준으로 작성된 글입니다. 요새 Node.js를 안하다보니... 최근에 3.X 버전이 상당히 변경되어 나온걸 확인해서 3.X 버전에는 맞지 않을 수 있습니다.RDBMS를 사용할 때 테이블 간의 참조를 통한 관계는 필수적이다. Node.js 진영에서 가장 핫한 ORM 라이브러리인 TypeORM에서 관계가 맺어져 있는 테이블을 다음 4가지 Join 하는 방법들을 통해 실제 쿼리는 어떻게 구성되는지 알아보자.
- find* + 옵션
- Lazy Loading
- Eager Loading
- Query Builder
환경설정
예제 데이터베이스는 PostgreSQL을 사용하고 team과 member라는 테이블을 다음과 같이 준비한다.
CREATE TABLE team( id serial PRIMARY KEY, "name" varchar(50), description text, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE member( id serial PRIMARY KEY, team_id integer REFERENCES team, "name" varchar(50), email varchar(255) UNIQUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
1개의 team, 3개의 member 데이터를 생성해놓는다.
위 테이블 구조를 표현하는 Entity는 다음과 같이 구현한다.
@Entity() class Team { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() description: string; @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) createdAt: Date; }
@Entity() class Member { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ unique: true }) email: string; @ManyToOne(() => Team) @JoinColumn({ name: 'team_id' }) team: Team; @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) createdAt: Date; }
find* 메소드 + 옵션
먼저 member 한 명을 조회해보자.
실제 쿼리를 확인하기 위해서 connection의 옵션 중 logging을 true로 설정하였다.
const memberRepository = connection.getRepository(Member);
const member = await memberRepository.findOne(1); console.log(member);
Member Entity에 relation이 명시되어있지만, repository의 그냥 find 메소드는 join 쿼리를 날리지 않는다.
다음과 같이 relations 옵션에 join 할 프로퍼티를 알려주면 left join 쿼리가 만들어지는 것을 확인할 수 있다.
const member = await memberRepository.findOne(1, { relations: ['team'] // <- }); console.log(member);
다음처럼 join 옵션을 줘서 alias까지 지정할 수 있게 쿼리를 날릴 수도 있다.
const member = await memberRepository.findOne(1, { join: { // <- alias: 'm', leftJoinAndSelect: { team: 'm.team' } } }); console.log(member);
Eager Loading
Entity의 relation을 정의할 때, eager라는 옵션을 줄 수 있다. 이 eager 옵션을 true로 설정하는 것은, find* 메소드로 조회할 때 이 컬럼을 join 해줘 를 알려주는 것이다.
@Entity() class Member { ... @ManyToOne(() => Team, { eager: true }) // <- @JoinColumn({ name: 'team_id' }) team: Team; ... }
const member = await memberRepository.findOne(1); console.log(member);
relations: ['team'], join: { ... }
옵션을 준 것과 같은 쿼리를 생성하는 것을 확인할 수 있다(team 테이블의 alias가 다르긴 하다).find* 메소드가 아닌 Query Builder를 통한 쿼리에는 Eager loading이 적용되지 않는다.
Lazy Loading
Eager Loading과 다르게 Lazy Loading은 해당 엔티티를 이미 조회한 이후에, 추가적으로 relation 데이터를 가져올 때 사용된다.
Lazy Loading을 구현하기 위해서는 relation 프로퍼티를 Promise로 만들어 주어야 한다.
@Entity() class Member { ... @ManyToOne(() => Team) @JoinColumn({ name: 'team_id' }) team: Promise<Team>; // <- ... }
Lazy Loading의 동작을 확인하기 위해 다음과 같이 코드를 실행해보았다.
const member = await memberRepository.findOne(1); console.log(member); console.log(member.team);
첫 번째 쿼리는 join이 없이 id로 member만 조회하는 쿼리가 실행되었다. 이때 member.team이 pending된 Promise인 것을 확인할 수 있다. 해당 promise를 resolve 하면 team을 조회할 수 있다.
const team = await member.team; console.log(team);
이 promise는 resolve될 때 team 테이블에 member를 Inner join 하는 쿼리를 실행한다. Lazy loading으로 조회를 하게 되면 쿼리를 2번에 나눠서 실행하게 된다.
Query Builder
위 방식들은 간단하게 join 쿼리를 날릴 수 있다는 장점이 있지만, join 할 컬럼들을 select 할 수 없다는 단점이 있다.
대부분의 경우에 필요하지 않은 컬럼까지 가져올 필요가 없기 때문에, 조회 쿼리는 QueryBuilder를 사용하게 된다.
먼저 QueryBuilder에서 단순하게 join만 하려면 *joinAndSelect 메소드를 사용할 수 있다.
const member = await memberRepository.createQueryBuilder('m') .leftJoinAndSelect('m.team', 't') .where('m.id = :id', { id: 1 }) .getOne(); console.log(member);
일부 컬럼들만 join 하려면 select와 *join 메소드를 사용할 수 있다.
const member = await memberRepository.createQueryBuilder('m') .select(['m.id', 'm.name', 'm.team', 't.id']) .leftJoin('m.team', 't') .where('m.id = :id', { id: 1 }) .getOne(); console.log(member);
member의 id와 name, team의 id와 name 만 가져온 것을 확인할 수 있다.
번외) join 하지 않고 외래 키 값만 가져오기
맨 처음에 했던 것처럼 그냥 find* 메소드를 이용해서 조회를 하게 되면 join을 하지 않고 조회를 하게 되는데, 이 때 team_id 컬럼 값은 엔티티에 매핑되지 않는 것을 확인할 수 있다.
외래키로 설정한 값을 조인 없이 가져오려면 엔티티를 다음과 같이 엔티티를 수정하면 된다.
@Entity() class Member { ... @Column({ name: 'team_id' }) // <- teamId: number; // <- @ManyToOne(() => Team) @JoinColumn({ name: 'team_id' }) team: Team; ... }
find*메소드를 통해 teamId라는 프로퍼티로 외래 키 값을 잘 가져오는 것을 확인할 수 있다.
const member = await memberRepository.findOne(1); console.log(member);
반응형'Javascript, Typescript' 카테고리의 다른 글
[Jest] object에 대한 다양한 matcher 함수들 (0) 2021.12.31 [TypeORM] repository.save()의 동작과 upsert() (5) 2021.12.19 [JS] Trailing Commas에 대한 고찰 (feat.ESLint) (0) 2021.12.05 [JS] 비동기 작업들의 순차실행과 병렬실행 (0) 2021.11.09 [TS] Typescript의 메소드 오버로딩 (0) 2021.11.04