[Prisma]カラムを他の型に変更したい時はmigration.sqlに移行クエリを書くのがよい。

に公開

Prismaでカラムを追加しようとしてprisma.schemaを見直していたら、Enum型で想定していたカラムがよく見たらEnum型ではなくString型になっていました。うっかりミスです。このままでも運用できなくはないですが気持ちが晴れないので、今まで文字列で保存していたカラムをEnumに移行することにしました。

参考

https://www.prisma.io/docs/orm/prisma-migrate/workflows/customizing-migrations

カラム名を変更する場合

ドキュメントの例。例えば以下のようなschemaがあったとして、

model Profile {
  id       Int    @id @default(autoincrement())
  biograpy String
  userId   Int    @unique
  user     User   @relation(fields: [userId], references: [id])
}

"biograpy"(ぐらぴー)と打ち間違えている箇所を修正したいので以下のように書き換えます。

model Profile {
  id        Int    @id @default(autoincrement())
  biography String
  userId    Int    @unique
  user      User   @relation(fields: [userId], references: [id])
}

このままnpx prisma migrate devしても良いですが、このままだと以下のようmigration.sqlが生成されます。

ALTER TABLE "Profile" DROP COLUMN "biograpy",
ADD COLUMN  "biography" TEXT NOT NULL;

これではbiograpyカラムを削除し、新しいbiographyカラムを作成してしまうため、データが消えてしまいます。
なので、一度マイグレーション用のSQLを生成だけした状態にして生成されたマイグレーション用SQLを編集して理想的なクエリにしてあげる必要があります。--create-onlyのオプションをつけてあげればSQLを生成した段階で作業を終えてくれます。

npx prisma migrate dev --name rename-migration --create-only

これで生成したSQLを以下のように編集します。この書き方なら正しく名前の変更のみを行うことができます。

ALTER TABLE "Profile" RENAME COLUMN "biograpy" TO "biography";

あとは変更を反映してあげればよし。ここまでがドキュメントの話です。

npx prisma migrate dev --name rename-migration

StringからEnumに変更するには?

ここからは以上の内容を踏まえて私の方で行ったことの紹介になります。今回はすでにデータは入っているが、全て同じ文字列が入っているカラムをEnumに移行しました。例えば、今後機能追加で"apple"以外にも"banana"と"cherry"も入ってくる予定だったfruitsカラムを、うっかり文字列にしていた状態です。運用できなくもなさそうですが、パターンが限られた文字列はEnumにしておきたい。

まずprisma.schemaを変更後の理想的な形に変更します。

model Sample {
    id      Int     @id              default(autoincrement)
-   fruits  String
+   fruits  Fruits  @default(apple)
}

Enum Fruits {
    apple
    banana
    cherry
}

この状態で--create-onlyでmigration.sqlを作成。

npx prisma migrate dev --name convert-fruits-to-enum --create-only

migration.sqlを以下のように編集。このSQLの中で移行作業がすべて行えるためスムーズです。

-- 1) 新しい Enum 型を作成
CREATE TYPE "Fruits" AS ENUM ('apple', 'banana', 'cherry');

-- 2) 一時的な Enum カラムを追加(NOT NULL にしておく)
ALTER TABLE "sample"
  ADD COLUMN "fruits_new" "Fruits" NOT NULL DEFAULT 'apple';

-- 3) 既存データを 'apple' に更新(本件ではすべて 'apple' なのでデフォルトのままでも OK)
--    念のため、すべての行を 'apple' にセットします。
UPDATE "sample"
SET "fruits_new" = 'apple'::"Fruits";

-- 4) 旧カラムを削除
ALTER TABLE "sample"
  DROP COLUMN "friuts";

-- 5) 一時カラムを元の名前にリネーム
ALTER TABLE "sample"
  RENAME COLUMN "fruits_new" TO "friuts";

-- 6) (オプション)もし NOT NULL や DEFAULT 'apple' をもう一度厳格に設定したい場合は以下を実行
ALTER TABLE "sample"
  ALTER COLUMN "fruit" SET DEFAULT 'apple',
  ALTER COLUMN "fruit" SET NOT NULL;

migrate devして完了。

npx prisma migrate dev

Expand & Contract パターン

より大きな規模のデータ移行については「Expand & Contract パターンを使ってダウンタイムなしでスキーマを変更しよう」ということ。”Expand & Contract パターン”というのが、

  1. 一つカラムを追加して、追加した新カラムの方に移行後のデータを入れる。(create,updateは新カラムに追加してfindなどは移行前のカラムを使用する。)
  2. 移行中は移行前のカラムを変わらず参照させる。
  3. タイミングで移行前のカラムを削除しデータ移行の完了。

という段階を踏んでデータ移行を行う手法とのことでした。
今回はそこまで大掛かりな移行作業ではなかったので前述のような移行作業を行いました。

まとめ

今回はプロジェクトがまだ小さいうちだったので変更の手間も小さくてラッキーでした。
この内容がわかっていれば今後の変更にも立ち向かえそうな気がしたので、小規模なうちに遭遇できてよかったです。コードの確認は大事。

Discussion