[Nest.js] TypeORM 적용
Wellboost 개인 프로젝트에는 TypeORM이라는 ORM을 적용해보려고 한다. express.js 때와는 달리 엔티티(모델)의 개념이 존재하기에 이를 좀 더 쉽게 관리할 수 있고 마이그레이션 또한 엔티티의 수정, 추가에 따라서 관리할 수 있도록 하기 위해 선택했다.
설치
npm install @nestjs/typeorm
npm install @nestjs/config
npm install dotenv
일단 두 모듈을 설치해주었다. dotenv와 config를 둘 다 설치하였는데 마이그레이션을 위한 config 설정과 app.module에 대한 설정에 각각 다른 환경변수 모듈을 적용했기에 두 모듈을 설치했다. 세부적인 것은 아래에서 설명하겠다.
경로
앞서 나가기 전, 나의 경로는 현재 다음과 같다.
src
| - config
| | - typeorm.config.ts
| - user
| | - controller
| | | -
| | - dto
| | | -
| | - entity
| | | - user.entity.ts
| | - repository
| | | -
| | - service
| | | -
|---- app.module.ts
config
일단 마이그레이션을 위한 DataSource 객체를 하나 생성한다.
import { config } from "dotenv";
import { DataSource } from "typeorm";
config();
const AppDataSource = new DataSource({
type: 'mysql',
host: process.env.MYSQL_HOST,
port: parseInt(process.env.MYSQL_PORT),
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
synchronize: false,
entities: ['**/*.entity.ts'],
migrations: ['src/migrations/*-migration.ts'],
migrationsRun: false,
logging: true,
});
export default AppDataSource;
여기서는 nest.js가 해당 모듈을 관리하고 있지 않기 때문에 nest/config가 아닌 dotenv를 사용해야한다. configService를 new 연산자를 통해 실행해도 nest.js가 제대로 읽어오지 못하기 때문에 꼭 알아둔다.
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UserModule,
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('MYSQL_HOST'),
port: configService.get<number>('MYSQL_PORT'),
username: configService.get<string>('MYSQL_USER'),
password: configService.get<string>('MYSQL_PASSWORD'),
database: configService.get<string>('MYSQL_DATABASE'),
entities: ['**/*.entity.ts'],
migrations: ['src/migrations/*-migration.ts'],
synchronize: false,
migrationsRun: false,
logging: true,
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
그리고 app.module.ts 파일도 다음과 같이 작성했는데 여기는 @Module 데코레이터가 존재하여 nest.js가 관리하고 있기에 nest/config를 사용했다. synchronize 같은 경우, nest.js 공식 docs에서도 추천하진 않기에 false로 둔다.
실행
"scripts": {
"migration:generate": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:generate -d ./src/config/typeorm.config.ts ./src/migrations/migration",
"migration:create": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:create -d ./src/config/typeorm.config.ts ./src/migrations/migration",
"migration:run": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run -d ./src/config/typeorm.config.ts",
"migration:revert": "npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:revert -d ./src/config/typeorm.config.ts",
//...
package.json에 다음과 같은 스크립트 문을 추가한다. 귀찮게 길게 써야할 것을 간단하게 스크립트로 실행시킬 수 있게 된다. 이렇게 까지만 하고 다음과 같이 실행시켜보자.
npm run migration:generate
> wellboost-api@0.0.1 migration:generate
> npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:generate -d ./src/config/typeorm.config.ts ./src/migrations/migration
query: SELECT VERSION() AS `version`
Error during migration generation:
MissingPrimaryColumnError: Entity "User" does not have a primary column. Primary column is required to have in all your entities. Use @PrimaryColumn decorator to add a primary column to your entity.
at EntityMetadataValidator.validate (/Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/metadata-builder/EntityMetadataValidator.ts:59:19)
at /Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/metadata-builder/EntityMetadataValidator.ts:43:18
at Array.forEach (<anonymous>)
at EntityMetadataValidator.validateMany (/Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/metadata-builder/EntityMetadataValidator.ts:42:25)
at DataSource.buildMetadatas (/Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/data-source/DataSource.ts:730:33)
at async DataSource.initialize (/Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/data-source/DataSource.ts:263:13)
at async Object.handler (/Users/choeseungdae/Desktop/project/wellboost-api/node_modules/src/commands/MigrationGenerateCommand.ts:87:13)
에러가 발생할 것이다. 한 번 읽어보면 User entity에 PK 값이 존재하지 않는다는 뜻이다.
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
user_idx: number;
@Column()
id: string;
@Column()
password: string;
}
그래서 일단은 테스트용으로 PK 값과 2개의 컬럼을 추가해보았다.
Migration /Users/Desktop/project/wellboost-api/src/migrations/1735843169807-migration.ts has been generated successfully.
이렇게 나왔다면 스크립트에 지정한 경로대로 마이그레이션 파일을 생성해줄 것이다. 실제로는 아직 커밋단계에 들어가지 않고 생성만 되었을 것이다.
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migration1735843169807 implements MigrationInterface {
name = 'Migration1735843169807'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`user\` (\`user_idx\` int NOT NULL AUTO_INCREMENT, \`id\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, PRIMARY KEY (\`user_idx\`)) ENGINE=InnoDB`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`user\``);
}
}
이런 식으로 up, down이라는 비동기 함수가 생성된 것을 알 수 있다. 아마 typeORM뿐만 아니라 knex도 이와 비슷한데, up은 적용시킬 DDL이고 down은 롤백의 느낌인 DDL이라고 보면된다.
여튼 DB 툴로 확인해보면 내가 임시로 추가한 User의 엔티티를 볼 수 없다. 이제 변경점에 대해 커밋을 적용해주면 된다.
npm run migration:run
> wellboost-api@0.0.1 migration:run
> npx ts-node -P ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run -d ./src/config/typeorm.config.ts
query: SELECT VERSION() AS `version`
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'wellboost' AND `TABLE_NAME` = 'migrations'
query: CREATE TABLE `migrations` (`id` int NOT NULL AUTO_INCREMENT, `timestamp` bigint NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB
query: SELECT * FROM `wellboost`.`migrations` `migrations` ORDER BY `id` DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations must be executed.
query: START TRANSACTION
query: CREATE TABLE `user` (`user_idx` int NOT NULL AUTO_INCREMENT, `id` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`user_idx`)) ENGINE=InnoDB
query: INSERT INTO `wellboost`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1735843169807,"Migration1735843169807"]
Migration Migration1735843169807 has been executed successfully.
query: COMMIT
정상적으로 커밋이 되었다니 확인해보자.
오잉? 두 가지의 테이블이 생성되었다. user는 내가 생성했던 테이블일 것이고 migrations? 이 파일이 나중에 체크섬의 역할을 해줄 것이다. 마이그레이션의 주된 역할이 무엇일지 생각해보자.
마이그레이션을 하는 맹목적인 이유는 나는 크게 2가지로 보고 있다. 첫째는 테이블의 스키마 변경 등의 로그성, 둘째는 공통된 마이그레이션이 적용된 환경이라고 생각한다. 마이그레이션 파일은 어떤 누군가는 생성된 파일이 있고 어떤 누군가는 존재하지 않으면 아마 실행되지 않을 것이다.
migrations의 테이블로 변경 내역에 대한 무결성을 보호하고 있기에 하나라도 존재하지 않는다면 마이그레이션을 무작정 적용시키지 않고 에러를 리턴해줄 것이다. 그렇기에 외부에서 드러나선 안되지만 협력을 하고있는 개발자들끼리는 공유를 하고 있어야한다.
여튼 이렇게 typeORM을 설치하고 마이그레이션을 적용해보았다.
참고
How to Generate and Run a Migration Using TypeORM in NestJS
Hello my friends! I hope you’re all fine. Today we’re going to unveil the mystery behind the generation and running of migrations in NestJS…
medium.com