프레임워크/TypeORM

TypeORM 0.3에서 migration 해보기

SpaceCowboy 2023. 2. 9. 22:41

Migration

migration이란?

TypeORM에서 synchronize: true 기능을 사용하게 되면 Entity의 수정사항을 자동으로 DB에 갱신시켜줍니다. 이 기능이 편한건 맞지만 간혹 특정 데이터를 아예 밀어버릴 수 있기 때문에 production 레벨에서는 안전하지 않습니다.

그래서 보통 백엔드 관련 프레임워크에서는 Migration이라는 기능을 제공합니다.

TypeORM에서는 데이터베이스 스키마를 업데이트하는 sql 쿼리를 모은 하나의 파일을 migration이라고 합니다.

이번 섹션에서는 Nest.js 환경에서 typeorm을 쓸 때 migration을 어떻게 진행할 수 있는지 서술합니다.

migration을 위한 셋업

우선 아래의 의존성들을 설치합니다. ts-nodetsconfig-paths입니다.

npm install -D ts-node tsconfig-paths

그리고 루트 디렉토리에 data-source.ts 라는 파일을 만들고 다음 코드를 작성합니다.

import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';

config();

const configService = new ConfigService();

export default new DataSource({
  type: 'mysql',
  host: configService.get('DB_HOST'),
  port: configService.get<number>('DB_PORT'),
  username: configService.get('DB_USERNAME'),
  password: configService.get('DB_PASSWORD'),
  database: configService.get('DB_DATABASE'),
  synchronize: false,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/database/migrations/*.ts'],
  migrationsTableName: 'migrations',
});

파일 이름이나 코드 내용은 어느 정도 커스텀하셔도 상관없습니다.

그리고 package.jsonscriptsTypeORM migration 관련 스크립트를 넣습니다.

"scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
        // typeorm 스크립트
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --dataSource ./data-source.ts",
    "migration:create": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create ./src/database/migrations/Migration",
    "migration:generate": "npm run typeorm migration:generate ./src/database/migrations/Migration",
    "migration:run": "npm run typeorm  migration:run",
    "migration:revert": "npm run typeorm migration:revert",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },

TypeORM의 공식문서 설명과는 좀 다른 스크립트입니다. TypeORM CLI가 절대경로를 이해하지 못해서 공식문서대로 따라하다가는 can not found module 같은 에러를 맞닥뜨리게 될겁니다. 그래서 임의로 절대경로를 이해시키기 위해 ts-node -r tsconfig-paths/register 가 들어간 것을 볼 수 있습니다.

ts-config-paths 모듈에 관한 내용은 여기에서 확인하실 수 있습니다.

아무튼 여기서 npm migration:generate 명령어를 실행해보시고 대충 아래 화면처럼 뜨면 성공한겁니다.

migration 해보기

먼저 처음에 다음 entity가 존재하고 DB에도 적용되어있다고 가정합니다.

import { BaseEntity } from 'src/app/base/base.entity';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Cat extends BaseEntity {
  @PrimaryGeneratedColumn()
  idx: number;

  @Column()
  name: string;
}

여기서 고객의 요구로 (가정) Catkind 컬럼을 추가합니다.

import { BaseEntity } from 'src/app/base/base.entity';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Cat extends BaseEntity {
  @PrimaryGeneratedColumn()
  idx: number;

  @Column()
  name: string;

  @Column()
  kind: string;
}

기존에는 typeorm의 synchronize 기능을 통해 자동으로 변경되었겠지만 이번에는 synchronize 기능을 끄고 migration 방식으로 진행해봅시다.

위의 migration을 위한 셋업 과정을 잘 진행했다면 npm run migration:generate 명령어를 터미널에서 실행했을 때 src/database/migartions 디렉토리에 {timestamp}-Migration.ts파일이 생성된 것을 보실 수 있습니다.

import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migration1675868794851 implements MigrationInterface {
  name = 'Migration1675868794851';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE \`cat\` ADD \`kind\` varchar(255) NOT NULL`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE \`cat\` DROP COLUMN \`kind\``);
  }
}
  • up은 npm run migration:run
  • down은 npm run migration:revert

실제로 npm run migration:run을 실행하면 터미널에서 다음과 같이 진행될겁니다.

이렇게 성공했다고 뜨면 migration을 완료한겁니다!

migration 파일 생성할 때 create vs generate

아마 공식문서를 보면 migration 파일을 만드는 방법이 generate 말고도 create 방식이 있다는걸 알 수 있습니다.

npm run migration:create 명령어를 실행하면 다음과 같은 파일이 생성됩니다.

// src/database/migrations/1675869805076-Migration.ts

import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migration1675869805076 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {}

  public async down(queryRunner: QueryRunner): Promise<void> {}
}

generate와는 달리 up과 down 메서드 부분에 내용이 아무것도 없습니다.

  • generate: 읽어들인 모든 entity 파일과 실제 데이터베이스를 비교해서 테이블을 추가 및 수정하는 migration 파일을 생성합니다.
  • create: 빈 migration 파일을 생성합니다. 따라서 스스로 테이블을 수정시키는 sql 쿼리문을 작성해야합니다.

generate는 컬럼을 아예 DROP 시키고 나서 재생성하는 쿼리문을 작성해줄 때가 종종 있습니다. 따라서 generate 방식을 쓸 땐 쿼리문을 확인해서 DROP하는 쿼리문이 있다면, 직접 모두 수동으로 작성하는 create 방식으로 진행하는게 좋습니다.

migration 파일 위치 src vs dist 비교

import { ConfigService } from '@nestjs/config';
import { config } from 'dotenv';
import { DataSource } from 'typeorm';

config();

const configService = new ConfigService();

export default new DataSource({
  type: 'mysql',
  host: configService.get('DB_HOST'),
  port: configService.get<number>('DB_PORT'),
  username: configService.get('DB_USERNAME'),
  password: configService.get('DB_PASSWORD'),
  database: configService.get('DB_DATABASE'),
  synchronize: false,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/database/migrations/*.ts'],
  migrationsTableName: 'migrations',
});

여기서 migrations: ['src/database/migrations/*.ts'] 부분에 의문이 갔던게, 저기를 dist 폴더 기준으로 하시는 분들도 좀 있던거 같습니다. (정확히는 기억 안남)

src 폴더 기준으로 넣어야할지 dist 폴더 기준으로 넣을지 고민이 되어서 뭐가 좋을지 고민해봤는데 그냥 src 폴더 기준으로 넣는게 좋을거 같습니다. 다음은 chatGPT 답변을 기용한겁니다.

💡

이는 TypeORM이 개발 단계에서 사용하는 소스 코드를 기준으로 마이그레이션을 수행할 수 있기 때문입니다. dist 기준으로 설정하면 빌드 과정에서 생성된 파일을 기준으로 마이그레이션을 수행하기 때문에, 소스 코드와 빌드 결과가 맞지 않을 경우 문제가 발생할 수 있습니다.