Oteto Blogのロゴ

NestJS × TypeORM 0.3 でCLIからmigrationする

前提

下記記事の構成で環境構築した前提で進める。

NestJS(Fastify) × TypeORM × PostgreSQL × Dockerで環境構築

手順

1. Userエンティティの実装

nest g res users --no-spec
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes

まずCRUD generatorでusersリソースを追加する。

(※本記事で使用するのはuser.entity.tsのみなので、その他に作成されたuser.controller.tsuser.service.ts、DTOなどは、クラスの中身を空にするなり削除するなりして問題ない)

import {
  Entity,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  Timestamp,
  Column,
} from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  readonly id?: number;

  @CreateDateColumn({ name: 'create_at' })
  readonly createdAt?: Timestamp;

  @UpdateDateColumn({ name: 'updated_at' })
  readonly updatedAt?: Timestamp;

  @Column({ unique: true })
  email: string;

  @Column({ name: 'hashed_password' })
  hashedPassword: string;
}

user.entity.tsにて、上記のようにカラムを設定する。

2. データソースの設定

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';

export const ENTITIES_DIR = 'dist/**/*.entity.js';
export const MIGRATION_FILES_DIR = 'dist/database/migrations/*.js';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get<string>('DATABASE_HOST'),
        database: configService.get<string>('DATABASE_NAME'),
        username: configService.get<string>('DATABASE_USER'),
        password: configService.get<string>('DATABASE_PASSWORD'),
        port: Number(configService.get<string>('DATABASE_PORT')),
        entities: [],
        entities: [ENTITIES_DIR],
        synchronize: false,
        migrations: [MIGRATION_FILES_DIR],
      }),
    }),
  ],
})
export class DatabaseModule {}

今回作成していくデータソースファイルだが環境構築で作成したsrc/database/database.module.tsと重複する設定があるため、上記のように修正する。

  • 先ほど定義したUserEntityを指定しつつ、変数化してexportする
  • migrationsにマイグレーションファイルのパスを指定
import { DataSource } from 'typeorm';
import { ENTITIES_DIR, MIGRATION_FILES_DIR } from './database.module';
import 'dotenv/config';

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DATABASE_HOST,
  database: process.env.DATABASE_NAME,
  username: process.env.DATABASE_USER,
  password: process.env.DATABASE_PASSWORD,
  port: Number(process.env.DATABASE_PORT),
  entities: [ENTITIES_DIR],
  synchronize: false,
  migrations: [MIGRATION_FILES_DIR],
});

データソースファイルであるsrc/database/database-source.tsを作成し、先ほどexportしたものをこちらでも指定する。

3. マイグレーションファイルの作成

docker exec -it app sh
mkdir -p src/database/migrations

コンテナ内に入り、専用のディレクトリを作成。

npx typeorm-ts-node-commonjs migration:generate -d src/database/database-source.ts --pretty src/database/migrations/CreateUser

上記コマンドを叩く。(--prettyを付与し、作成されるマイグレーションファイル内のSQL文を複数行にしている)

import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateUser1695999831504 implements MigrationInterface {
    name = 'CreateUser1695999831504'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`
            CREATE TABLE "user" (
                "id" SERIAL NOT NULL,
                "create_a" TIMESTAMP NOT NULL DEFAULT now(),
                "updated_at" TIMESTAMP NOT NULL DEFAULT now(),
                "email" character varying NOT NULL,
                "hashed_password" character varying NOT NULL,
                CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")
            )
        `);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`
            DROP TABLE "user"
        `);
    }

}

src/database/migrations/xxxxxxxxxxxxx-CreateUser.tsが作成され、今回追加したUserのテーブルをCREATEするSQLが出力されていることが確認できる。

4. マイグレーションの実行

npx typeorm-ts-node-commonjs migration:run -d src/database/database-source.ts

上記コマンドを叩く。

query: SELECT * FROM current_schema()
query: SELECT version();
query: SELECT * FROM "information_schema"."tables" WHERE "table_schema" = 'public' AND "table_name" = 'migrations'
query: CREATE TABLE "migrations" ("id" SERIAL NOT NULL, "timestamp" bigint NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY ("id"))
query: SELECT * FROM "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" (
                "id" SERIAL NOT NULL,
                "create_a" TIMESTAMP NOT NULL DEFAULT now(),
                "updated_at" TIMESTAMP NOT NULL DEFAULT now(),
                "email" character varying NOT NULL,
                "hashed_password" character varying NOT NULL,
                CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")
            )
        
query: INSERT INTO "migrations"("timestamp", "name") VALUES ($1, $2) -- PARAMETERS: [1695999831504,"CreateUser1695999831504"]
Migration CreateUser1695999831504 has been  executed successfully.
query: COMMIT

先ほど作成したマイグレーションファイルに沿ってクエリが実行された。

Userテーブルが作成されカラムも定義されている

pgAdminを見てみると、usersテーブルが作成されカラムも定義されていることが確認できる。