TypeORM

seongha_h·2024년 12월 30일

NestJS

목록 보기
3/3

ORM

Object Relational Mapper 입니다. 객체와 관계형 데이터 베이스의 데이터를 매핑하는 역할을 합니다.
ORM을 이용하면 SQL 문을 직접 작성하지 않고 ORM이 적절한 SQL문으로 변환하여 사용하게 됩니다. 이는 데이터 베이스 변경시 코드에 존재하는 SQL문을 변경하는 리소스를 줄일 수 있다는 장점이 있습니다.

ORM을 왜 사용할까요?

  • 객체지향 언어로 데이터를 쉽게 조작할 수 있도록 도와줍니다.
  • 코드의 가독성을 높이고, 유지보수를 용이하게 합니다.
  • 데이터 베이스와의 결합도를 낮춥니다.

하지만 ORM이 만능은 아닙니다.

  • 복잡한 쿼리에서 ORM이 생성하는 쿼리의 성능이 비효율적일 수 있습니다.
  • 학습하는데 시간이 필요합니다.
  • 모든 SQL의 기능을 제공하지 않기에 직접 작성해야하는 경우가 존재합니다.

이러한 장단점을 통해 ORM 사용 여부를 적절히 선택해야 합니다.

TypeORM

TypeORM은 TypeScript 진영에서 사용하는 대표적인 ORM입니다. JavaScript에서도 사용할 수 있지만 TypeScript의 타입 지원을 사용하지 못하기 때문에 제약사항이 있을 수 있습니다.

시작

typeORM을 NestJS에서 사용하기 위해서는 데이터 베이스 모듈을 만들고 사용합니다. 이는 데이터 베이스 관련 로직을 분리시키고, 다른 여러 모듈에서 재사용하기 위함입니다.
또한, 아래 설정 값들을 환경변수를 이용하여 보안을 향상시킬 수 있습니다.

//database.provider.ts
import { DataSource } from 'typeorm';

export const databaseProviders = [
  {
    provide: 'DATA_SOURCE',
    useFactory: async () => {
      const dataSource = new DataSource({
        type: 'mysql',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: 'root',
        database: 'test',
        entities: [
            __dirname + '/../**/*.entity{.ts,.js}',
        ],
        synchronize: true,     
      });

      return dataSource.initialize();
    },
  },
];

//databaseModule.ts
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

synchronize 옵션은 true로 설정시 데이터 베이스를 초기화 합니다.
따라서 운영 환경에서는 false로 두고 사용해야 합니다.

Entity

엔티티는 데이터베이스 테이블에 맵핑되는 클래스입니다. 아래와 같이 @Entity()를 이용하여 설정할 수 있습니다.
또한, 이런 엔티티들은 위의 설정에서 entities 에 포함되어야 합니다. 경로로 설정되거나, Class를 import 하는 형식으로 포함시키켜야 합니다.

  • @Column : 열
  • @PrimaryColumn : 기본키
  • @PrimaryGeneratedColumn : 기본키 + auto_increment
  • @CreateDateColumn : 테이블 생성시 자동으로 날짜 생성

위와 같이 컬럼을 다양한 옵션과 함께 지정할 수 있고, name 옵션을 통해 칼럼 명을 정의할 수 있습니다.

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    isActive: boolean
  
    @CreateDateColumn({ name: 'created_at' })
    createdAt: Date;
}

Relation

관계형 데이터 베이스를 맵핑하는 만큼 여러 관계를 설정할 수 있습니다.

  • @OneToOne을 사용한 1:1
  • @ManyToOne을 사용한 N:1
  • @OneToMany를 사용한 1:N
  • @ManyToMany를 사용한 N:M

1:N 관계

1:N 관계의 예시로 user가 여러장의 사진을 갖는경우를 보겠습니다.

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User"

@Entity()
export class Photo {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    url: string

    @ManyToOne(() => User, (user) => user.photos)
    user: User
}
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @OneToMany(() => Photo, (photo) => photo.user)
    photos: Photo[]
}

@ManyToOne / @OneToMany 관계에서는 @JoinColumn을 생략할 수 있습니다.
@JoinColumn 을 명시적으로 표시하는 이유는 column 명을 커스텀하거나, 복합 외래키거나 명시적인 코드 문서화가 필요할 때 사용합니다.

외래키를 가지는 쪽은 주로 1:N 관계에서의 N측입니다. 따라서 @ManyToOne 은 1:N 관계에서 필수적이며 @OneToMany 는 양방향 맵핑(참조)를 편리하게 하기 위해 사용하는 것입니다.

관계 매핑의 특징

  • @ManyToOne은 실제 외래 키를 가지는 쪽이므로 독립적으로 존재할 수 있습니다
  • @OneToMany는 실제 데이터베이스에서 외래 키를 가지지 않고, 단지 반대쪽 관계를 참조하는 것이므로 @ManyToOne 없이는 존재할 수 없습니다

@OneToMany는 @ManyToOne 없이는 존재할 수 없습니다. @OneToMany를 사용하려면 @ManyToOne이 필요합니다. 그러나 그 반대는 필수가 아닙니다. @ManyToOne 관계에만 관심이 있는 경우 관련 엔터티에 @OneToMany가 없어도 관계를 정의할 수 있습니다.

N:M 관계

import { Entity, PrimaryGeneratedColumn, Column }from "typeorm"

@Entity()
exportclass Category {
    @PrimaryGeneratedColumn()
    id:number

    @Column()
    name:string}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
}from "typeorm"
import { Category }from "./Category"

@Entity()
exportclass Question {
    @PrimaryGeneratedColumn()
    id:number

    @Column()
    title:string

    @Column()
    text:string

    @ManyToMany(() => Category)
    @JoinTable()
    categories: Category[]
}

@ManyToMany 관계에는 @JoinTable()이 필요합니다.
관계의 한쪽(소유) 쪽에 @JoinTable을 배치해야 합니다.

+-------------+--------------+----------------------------+
|                        category                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        question                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
| text        | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|              question_categories_category               |
+-------------+--------------+----------------------------+
| questionId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
| categoryId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

Eager, LAZY Loading

eager loading은 즉시로딩, lazy loading 은 지연 로딩을 의미합니다.
즉시로딩은 데이터 조회시 연관된 테이블까지 함께 가져오는 방법이고, 지연로딩은 연관 데이터를 함께 가져오지 않고 필요할 때 조회하여 가져오는 방법입니다.

즉시 로딩은 N+1문제를 피할 수 있지만 필요없는 데이터까지 함께 조회하여 성능이 저하될 수 있습니다. 지연로딩은 필요한 데이터만 가져오기에 성능 최적화를 할 수 있지만 N+1문제가 발생할 수 있기에 주의해야 합니다.

typeORM의 기본 방식은 즉시로딩입니다.

즉시로딩

//Category.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"
import { Question } from "./Question"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @ManyToMany((type) => Question, (question) => question.categories)
    questions: Question[]
}

//Question.ts
import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany((type) => Category, (category) => category.questions, {
        eager: true,
    })
    @JoinTable()
    categories: Category[]
}

지연 로딩

//Category.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"
import { Question } from "./Question"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @ManyToMany((type) => Question, (question) => question.categories)
    questions: Promise<Question[]>
}

//Question.ts
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany((type) => Category, (category) => category.questions)
    @JoinTable()
    categories: Promise<Category[]>
}

QueryBuilder

QueryBuilder 는 ORM의 핵심인 쿼리를 만들어주는 역할입니다. TypeScript 코드 내에서 SQL 쿼리를 생성하고 실행할 수 있어 가독성과 유지 관리가 용이합니다.
이를 이용하여 유연한 쿼리를 작성할 수 있습니다.

가장 기본적인 예시는 다음과 같습니다. 쿼리 동작 결과로 User 객체를 반환합니다.

const firstUser = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne()
    
//SQL
SELECT
    user.id as userId,
    user.firstName as userFirstName,
    user.lastName as userLastName
FROM users user
WHERE user.id = 1

//결과 
User {
    id: 1,
    firstName: "Timber",
    lastName: "Saw"
}

참고

더 자세한 내용은 아래 링크를 참고합니다.
https://typeorm.io/
https://docs.nestjs.com/recipes/sql-typeorm

profile
https://github.com/Fixtar

0개의 댓글