Sequelize Migration with TypeScript

민수빈·2023년 4월 9일
0

Migration

Sequelize는 기본적으로 Database Schem가 변경되면 적용하는 방식이 세 가지 존재한다.

Model.sync()

  • 테이블이 없으면 생성, 테이블이 있으면 No-op

Model.sync({ force: true })

  • 테이블이 없으면 생성, 테이블이 있으면 기존 테이블 drop 이후 생성

Model.sync({ alter: true })

  • 현재 테이블과 상태 비교 후 필요한 동작 실행 (column 존재 여부, column 타입 등)

(심지어 과거에는 alter 옵션조차 없었다. 🥲)

하지만 Sequelize 공식 문서에서 이야기하고 있듯 prod 환경에서 자동적인 alter에 맡기기에는 너무 불안전할 수 있다. 따라서 Migration을 사용하는 것을 권장하고 있다.

현재는 크게 두 가지를 활용하고 있다.

  • seeders : 기초 데이터 넣어주기
  • migrations : 변경된 데이터 스키마 적용

기초 설정

만약 디렉토리 구조를 sequelize init으로 생성하지 않은 경우는 sequelize-cli 명령어를 사용할 때 시드, 모델, 설정 파일 등 경로를 파악할 수 없기 때문에 기본 sequelize 설정에 맞춰주거나, .sequelizerc와 같은 설정파일을 통해 경로를 매칭해줘야한다.

Typescript를 사용한 환경은 Javascript 파일은 build 디렉토리 내부에 생성되므로 (tsc를 통해) 디렉토리 구조가 변경된다. 따라서 .sequelizerc 설정 파일을 통해 경로를 매핑한다.
(TS 파일 자체로 실행할 수 있는 방법 찾아보자.)

.sequelizerc

const path = require('path');

module.exports = {
  config: path.resolve('./build/config', 'config.json'),
  'models-path': path.resolve('./build', 'models'),
  'seeders-path': path.resolve('./build', 'seeders'),
  'migrations-path': path.resolve('./build', 'migrations')
};

참고사항으로 각 파일들의 위치는 아래와 같다. (컴파일된 JS 파일 기준)

  • 설정 파일 : ./build/config/config.json
  • 모델 파일들 위치 디렉토리 : /build/models/
  • 시드 파일들 위치 디렉토리 : /build/seeders/
  • 마이그레이션 파일들 위치 디렉토리 : /build/migrations/

이제 기본 설정이 끝났으니, seed 기능을 사용해보자.

Seed

Sequelize와 같이 ORM을 사용할 때는 정적 데이터(e.g. 기본 카테고리, 종류 등)를 일일히 넣어주기 힘들기 때문에 seed를 사용해서 정적 데이터의 삽입/삭제를 커맨드 기반으로 할 수 있도록 제공하고 있다.

프로젝트에 sequelize-cli를 설치해놓지 않았기 때문에 npx sequelize-cli ~ 와 같이 실행하지만 프로젝트에 실행되어있다면 sequelize-cli ~ 와 같이 실행하자.

npx sequelize-cli init:seeders

(seeders 디렉토리만 생성, init으로 실행할 경우 모든 디렉토리가 생성)

npx sequelize-cli seed:generate --name <파일명>

타임스탬프와 이름으로 시드 파일이 생성된다. (아래는 디렉토리 및 파일 생성 후 .ts로 변경한 상황)

기본적으로는 JS 파일이 생성되는데, .ts로 바꿔주고 필요 타입을 불러와서 다음과 같이 작성하자.

import { QueryInterface, Op } from 'sequelize';

module.exports = {
  async up(queryInterface: QueryInterface) {
    const categories = [
      { categoryKeyword: '식사' },
      { categoryKeyword: '미팅' },
      { categoryKeyword: '여행' },
      { categoryKeyword: '기타' }
    ];
    await queryInterface.bulkInsert('CategoryKeyword', categories);
  },

  async down(queryInterface: QueryInterface) {
    await queryInterface.bulkDelete('CategoryKeyword', {
      categoryId: {
        [Op.gte]: 0
      }
    });
  }
};

up 실행시 CategoryKeyword 테이블에 4개의 레코드를 삽입하고

down 실행시 모든 (id가 0보다 큰 조건) 레코드를 삭제한다.

이때 QueryInterface는 ORM을 거치지 않으므로 DB 내에서의 테이블명, 컬럼명을 사용해야함에 주의하자.

실제로 CategoryKeyword 모델은 다음과 같다.

@Table({ tableName: 'CategoryKeyword', modelName: 'CategoryKeyword' })
class CategoryKeyword extends Model {
  @PrimaryKey
  @AutoIncrement
  @Column({ type: DataType.INTEGER, field: 'categoryId' })
  id: number;

  @AllowNull(false)
  @Column({ type: DataType.STRING, field: 'categoryKeyword' })
  keyword: string;

  @HasMany(() => PromisingModel)
  promisings: PromisingModel[];
}

export default CategoryKeyword;

로직 내에서는 PK를 category.id와 같이 사용하지만 QueryInterface에서는 categoryId와 같이 작성했다.

이제 명령어를 통해 데이터를 넣어줄 수 있다.

tsc —build 
npx sequelize-cli db:seed:all

실행 취소는 아래 명령어로 할 수 있다.

npx sequelize-cli db:seed:undo

Migrations

이제 변경된 스키마를 적용시켜보자. 기본적인 과정은 위와 유사하다.

Migration 파일을 생성한다.

npx sequelize-cli migration:generate --name <파일명>

User에서 type이라는 문자열 컬럼을 추가하기 위한 Migration은 아래와 같다.
기존 데이터에서는 값을 default로 가지도록 추가한다.

'use strict';

import { DataTypes, QueryInterface } from 'sequelize';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up (queryInterface: QueryInterface) {
    /**
     * Add altering commands here.
     *
     * Example:
     * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
     */
    await queryInterface.addColumn('User', 'type', { type: DataTypes.STRING, defaultValue: 'default' });

  },

  async down (queryInterface : QueryInterface) {
    /**
     * Add reverting commands here.
     *
     * Example:
     * await queryInterface.dropTable('users');
     */
    await queryInterface.removeColumn('User', 'type');

  }
};

이제 Migration을 적용해야하는데, seeder와 달리 주의해야하는 점이 하나 더 있다.

Migration의 경우 이전에 어느 마이그레이션까지 적용했는지를 추적하기위해서 Database 테이블에 SequelizeMeta 테이블을 생성하고, 해당 테이블의 레코드를 기준으로 마이그레이션을 적용한다.

npx sequelize-cli db:migrate

따라서 위의 명령어를 실행하면 SequelizeMeta 테이블을 생성하고, 기존 SequelizeMeta 테이블에 존재하지 않은 마이그레이션 파일을 실행한다.

실행 이전 SequelizeMeta 테이블

실행 명령어 결과

실행 이후 SequelizeMeta 테이블

실행 이후에 대상 테이블을 확인해보면 원하던 컬럼이 추가된 것을 확인할 수 있다.

취소도 이전 시드와 유사하게 undo를 통해 할 수 있다. 특정 마이그레이션까지만 되돌리고 싶으면 --to 옵션을 사용하자.

npx sequelize-cli db:migrate:undo:all --to XXXXXXXXXXXXXX-<migration name>.js

단 스키마 변경 명령어는 DDL로 DML과 달리 트랜잭션 (롤백/커밋) 을 적용할 수 없어 중간에 실패하더라도 이전까지의 명령어가 모두 적용된다.
(DDL은 데이터베이스의 metadata를 건드리므로 DML과 달리 version 및 트랜잭션 관리가 어려울 수 있다고 한다.)

따라서 실제 배포 서버에 적용하기전까지 충분히 로컬 환경에서 do / undo 가 올바른 것 확인해봐야하는 점에 주의하자.

Reference

Using Sequelize with TypeScript - Michal Zalecki

NodeJS Express Typescript로 Sequelize환경구축

Sequelize QueryInterface

DDL Transaction 관련 글

profile
개발 기록. 이전 블로그 (알고리즘 위주) : https://blog.naver.com/tnqls5417

0개의 댓글