🚀

TypeORM+MySQLでmigrationのエラー時、query: ROLLBACKとログ出力されるけどDDLはロールバックされない

2022/04/14に公開

TypeORMのmigration機能でややハマりました。

確認した環境

  • TypeORM 0.2.29
  • MySQL 8.x
  • PostgreSQL 13.x

TypeORMのmigration機能

TypeORMのmigration機能では、デフォルトでトランザクションを張った状態で実行されます。
(migration時のトランザクションをOFFにしたい場合、オプションとして-t falseを指定すればよいです。)

migration:run コマンドを実行すると、query: START TRANSACTION とログ出力され、migrationファイルに定義した処理が順番に実行されます。
テーブル作成等のDDL、データ登録等のDMLを実行できます。

また、migrationにて作成しようとしたテーブルが既に存在する場合や、外部キー制約、SQLの構文エラー等、migration中に何かしらのエラーが発生した場合、query: ROLLBACKとログ出力されます。
そのため、エラー発生時には定義した一連の処理が正しくロールバックされ、元の状態となることを期待します。

が、MySQLを使用した環境では、DDLのロールバックはされません。
query: ROLLBACKと出力されるにも関わらず、です。

...なぜ?

ロールバックされない原因

MySQLのDDLはロールバックすることができないため、です。
(つまり、TypeORMのログ出力が適切ではない!?)

参考:https://dev.mysql.com/doc/refman/8.0/ja/cannot-roll-back.html

再現方法

既にuserテーブルが存在する場合、かつcompanyテーブルが存在しない場合で以下のmigrationファイルを用意します。

migrationファイル

import { MigrationInterface, QueryRunner } from "typeorm";

export class test1649923114889 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      "CREATE TABLE `company` (`id` int NOT NULL AUTO_INCREMENT, `companyName` varchar(255) NOT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB"
    );
    await queryRunner.query(
      "CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `userName` varchar(255) NOT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB"
    );
  }

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

migrationを実行します。
既にuserテーブルは作成されていないるので、想定される結果は以下の通り。

  1. companyテーブルのCREATE TABLE のSQLがトランザクション上で成功する
  2. userテーブルのCREATEのSQLはエラーとなる
  3. 1がロールバックされ、companyとuserのどちらのテーブルも作られない

以下、migration実行時のログの抜粋

$ ts-node -T node_modules/.bin/typeorm migration:run

...(中略)...

query: START TRANSACTION
query: CREATE TABLE `company` (`id` int NOT NULL AUTO_INCREMENT, `companyName` varchar(255) NOT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB
query: CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `userName` varchar(255) NOT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB
query failed: CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `userName` varchar(255) NOT NULL, `createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB
error: Error: Table 'user' already exists

...(中略)...

  code: 'ER_TABLE_EXISTS_ERROR',
  errno: 1050,
  sqlState: '42S01',
  sqlMessage: "Table 'user' already exists"
}
query: ROLLBACK
Error during migration run:
QueryFailedError: Table 'user' already exists

...(中略)...

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

ログから想定される結果も

  1. companyテーブルのCREATE TABLE のSQLがトランザクション上で成功する
  2. userテーブルのCREATEのSQLはエラーとなる
  3. 1がロールバックされ、companyとuserのどちらのテーブルも作られない

となりますが、結果はcompanyテーブルのみ作成された状態となります。
ログにはquery: ROLLBACKと出力されるので、3が正しく行われていないように見えます。

実際にはMySQLのDDLはロールバックできないため、3の想定は誤りかつ、TypeORMのログ出力が適切ではない、ということになります。

PostgreSQLの時の動作

MySQLとは異なりPostgreSQLでは想定通りの動作となります。
companyテーブル、userテーブルのどちらも作られていない状態にロールバックされます。

(migrationファイルはPostgreSQL用に用意します(省略))
migration実行時のログ(PostgreSQL)

$ ts-node --transpile-only node_modules/.bin/typeorm migration:run

...(中略)...

query: START TRANSACTION
query: CREATE TABLE "company" ("id" SERIAL NOT NULL, "companyName" character varying NOT NULL, "createdAt" character varying NOT NULL, "updatedAt" character varying NOT NULL, CONSTRAINT "PK_635c0seeabda8862d5b0237b42b4" PRIMARY KEY ("id"))
query: CREATE TABLE "user" ("id" SERIAL NOT NULL, "companyName" character varying NOT NULL, "createdAt" character varying NOT NULL, "updatedAt" character varying NOT NULL, CONSTRAINT "PK_635c0eeabada8862d5b0237b42b4" PRIMARY KEY ("id"))
query failed: CREATE TABLE "user" ("id" SERIAL NOT NULL, "companyName" character varying NOT NULL, "createdAt" character varying NOT NULL, "updatedAt" character varying NOT NULL, CONSTRAINT "PK_635c0eeabada8862d5b0237b42b4" PRIMARY KEY ("id"))
error: error: relation "user" already exists

...(中略)...

  length: 98,
  severity: 'ERROR',
  code: '42P07',

...(中略)...

query: ROLLBACK
Error during migration run:

...(中略)...

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

(あれ?エラーはrelationが既に存在する、ではなくてtableが既に存在する の方が適切...?)

ロールバックされない原因(再掲)

MySQLの仕様です。
https://dev.mysql.com/doc/refman/8.0/ja/cannot-roll-back.html

まさかログ出力が適切でないとは、、、という言い訳をしつつ、
ログ自体を疑うことも必要であることを学びました。
採用しているDBのドキュメントは読みましょうね。。。

なお、本件はTypeORMのリポジトリのissueにて報告、ログ出力の改善が提案されています。
https://github.com/typeorm/typeorm/issues/7054

以上です。

GitHubで編集を提案

Discussion