kakasoo

[TypeORM] 1. Column과 Entity, 일대 다 관계의 표현 본문

프로그래밍/TypeORM

[TypeORM] 1. Column과 Entity, 일대 다 관계의 표현

카카수(kakasoo) 2023. 1. 2. 23:50
반응형

특별한 Column Decorator

import { ApiProperty } from '@nestjs/swagger';
import { DeleteDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { CreateAtEntity } from './create-at.entity';

export class CommonEntity extends CreateAtEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @UpdateDateColumn()
    updatedAt: Date;

    @DeleteDateColumn({ nullable: true })
    deletedAt: Date;
}

TypeORM에서 Entity는 데이터베이스의 각 테이블을 JavaScript 코드로 해석한 것이에요.

그래서 @Column() 데코레이터를 통해서 각각의 칼럼들이 무엇을 의미하는지를 서술합니다.

@Column() 외에도 createdAt, updatedAt, deletedAt을 나타내기 위한 특별한 데코레이터들도 있습니다.

TypeORM에는 각각 @CreateDateColumn, @UpdateDateColumn, @DeleteDateColumn을 의미합니다.

또한 PK ( = Primary Key )를 나타내기 위한 @PrimaryGeneratedColumn 도 있습니다.

다만 이런 칼럼들은 모두 @Column() 데코레이터를 확장한 것이기 때문에, Column을 이해하는 게 더 중요합니다.

 

Column Decorator’s parameter

@Column('int4')
int: number;

TypeORM에서의 Column은 Column 데코레이터로 표현되며, 데코레이터의 첫 인자는 타입입니다.

 

@Column()
int: number;

하지만 TypeScript의 타입이 있다면 별도의 타입 명시를 하지 않더라도 int, varchar, bool 등이 들어갑니다.

 

@Column({ name: 'int_column_name' })
int: number;

만약 name property에 이름을 명시해준다면, code 상에서는 ‘int’ 지만, DB에서는 다른 이름임을 알려줄 수 있습니다.

TypeORM 역시 ORM의 의미인 Object Relation Mapping에 충실하게 구현되어 있습니다.

DB가 실제로 어떠하든, Node.js 객체로 구현된 클래스를 보고 DB 로직을 작성할 수 있게 되어 있습니다.

 

@Column({ nullable: true, unique: true, select: false })
int: number;

그 외에도 null을 허용할지를 결정하는 nullable과 테이블 내에서 유일한 값을 의미하는 unique 설정이 있고,

추후 이야기할 select 문에서 별도의 명시 없이는 자동으로 잡히지 않게 해주는 select 설정이 있습니다.

그 외에도 특정 DB에서 사용 가능한 array나 prefix, comment 등 재미있는 설정들이 다수 있습니다.

다만 이 설정들은, 필요한 시점에 배워도 늦지 않습니다.

Entity는 이렇게, 자바스크립트 클래스에 @Column() 데코레이터를 덧붙여 DB와 매핑한 것에 불과합니다.

 

Column을 가진 Entity 예시

import { Column, Entity, OneToMany } from 'typeorm';
import { CommonEntity } from '../common/common.entity';
import { ProductEntity } from './product.entity';

@Entity()
export class SellerEntity extends CommonEntity {
    @Column({ comment: '대표자 이름' })
    ownerName: string;

    @Column({ comment: '브랜드 이름' })
    brandName: string;

    @Column({ comment: '사업자번호' })
    brn: string;

    @Column({ length: 15, nullable: true, comment: '전화번호' })
    phone: string;
}

이제 이 클래스를 해석할 수 있게 되었습니다.

각각의 칼럼들은 이름이 동일한 이름으로 DB 매핑되어 Column의 name property는 생략되었습니다.

이 중 phone Column은 전체 길이가 최대 15를 넘지 않으며 null이 허용된 것을 알 수 있습니다.

또한 Entity는 JavaScript의 클래스기 때문에 상속을 통해 표현될 수 있습니다.

이 Seller Entity는 id, createdAt, updatedAt, deletedAt을 상속을 통해 표현하고 있습니다.

모든 Entity에 동일하게 들어가는 칼럼이기 때문에 이처럼 표현하는 것이 중복을 줄이는 데에 용이합니다.

 

관계 매핑, Relations

OneToMany, ManyToOne

import { Column, Entity, OneToMany } from 'typeorm';
import { CommonEntity } from '../common/common.entity';
import { ProductEntity } from './product.entity';

@Entity()
export class SellerEntity extends CommonEntity {
    @Column({ comment: '대표자 이름' })
    ownerName: string;

    @Column({ comment: '브랜드 이름' })
    brandName: string;

    @Column({ comment: '사업자번호' })
    brn: string;

    @Column({ length: 15, nullable: true, comment: '전화번호' })
    phone: string;

    /*
    * below are relations
    */
    @OneToMany(() => ProductEntity, (product) => product.seller)
    products: ProductEntity[];
}

DB 테이블은 서로가 서로에게 조인될 수 있도록 외래키를 주고 받는 형태로 작성되곤 합니다.

여기서 Seller ( = 판매자 ) 는 자신이 판매하고 있는 물건이 여럿 있을 겁니다.

이 경우 판매자가 상품 id를 가지는 것은 테이블 내에서는 표현이 불가능하기 때문에,

각각의 상품이 seller의 id 값을 FK ( Foreign Key ) 로 가지는 게 더 적절합니다.

어떤 대상 A가 B를 다수 가지고 있다는 것은, B 입장에서는 B 다수가 하나의 A를 가진다는 것과도 같습니다.

그래서 A에게 B는 일대다 관계이고, B에게 A는 다대일 관계라고 말할 수도 있어요.

TypeORM에서는 이런 관계 표현을 OneToMany, ManyToOne Decorator로 표현합니다.

 

@OneToMany(() => TargetEntity, (target) => taget.inverse)
@OneToMany(() => ProductEntity, (product) => product.seller)

OneToMany의 첫 번째 파라미터는 A ( = Seller )가 어느 Entity ( = Product )로 이어질 수 있는지를 나타냅니다.

그리고 두 번째 파라미터는 그 상대 Entity에서 A가 어떻게 표현되고 있는지를 담아줍니다.

우리는 아직 Product Entity를 보지 못했지만, Seller Entity에 표현된 역 (inverse)을 통해,

Product Entity Class에 seller 라는 이름의 프로퍼티가 정의되어 있다는 것을 짐작할 수 있습니다.

 

@ManyToOne과 @JoinColumn

import { Column, Entity, ManyToOne } from 'typeorm';
import { CommonEntity } from '../common/common.entity';
import { SellerEntity } from './seller.entity';

@Entity()
export class ProductEntity extends CommonEntity {
    @Column()
    sellerId: number;

    @Column({ comment: '상품 이름' })
    name: string;

    /*
    * below are relations
    */
    @ManyToOne(() => SellerEntity, (seller) => seller.products)
    @JoinColumn({ name: 'sellerId', referencedColumnName: 'id' })
    seller: SellerEntity;
}

반대쪽의 Product Entity를 간략하게 표현하면 이런 구조로 되어 있습니다.

각각의 product는 sellerId를 가지고 있고, 이를 통해서 자신의 판매자가 누군지 찾아갈 수 있게 되었습니다.

그리고 앞에서 짐작하셨다시피, product 여럿이 하나의 seller를 가지기 때문에 seller는 배열이 아닙니다.

 

/*
* below are relations
*/
@OneToMany(() => ProductEntity, (product) => product.seller)
products: ProductEntity[];

앞서 Seller Entity에서 products를 ProductEntity[] 로 표현한 것과 비교하면 차이가 명백합니다.

이는 Seller 입장에서는 일대다 관계였기 때문에 seller 하나에 연결된 상품이 다수였기 때문에 그렇습니다.

반대로 Product Entity 입장에서는 Seller는 다대일 관계기 때문에 SellerEntity가 타입이 될 수 밖에 없습니다.

또 다른 큰 차이는 Product Entity에서는 @JoinColumn() 이라는 decorator를 가졌다는 점입니다.

JoinColumn Decorator는 쉽게 말해, 이 seller 라는 TagerEntity가 어떻게 연결되었는지를 의미합니다.

name은 현재 보고 있는 Entity에서 어떤 Column을 통해 연결되었는지를 의미합니다.

Product Entity는 sellerId를 가지고 있고, 이걸 통해서 seller를 찾아갈 수 있다고 말씀드렸어요.

그러니깐, Product Entity의 JoinColumn options의 name 프로퍼티는 sellerId가 됩니다.

referencedColumnName은 역으로 sellerId가, Seller Entity에서는 어떤 Column을 참조했는지를 뜻합니다.

sellerId는 Seller Entity의 id이므로, ‘id’가 값이 됩니다.

반응형