Prisma Migrate の導入から各種実行方法について試してみた

## はじめに

Prisma Migrate のプロジェクトへの導入、データベース定義の更新、マイグレーションの実行やロールバックについて、実際にコマンドを使って試してみました。

## 新規プロジェクトで使い始める

新規プロジェクトで Prisma Migrate を使って、データベースを管理します。今回は公式のチュートリアルを参考に、`MySQL` を使って実際に試してみました。

https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-postgresql

※ MySQL を利用する場合の接続情報については[こちらのドキュメント](https://www.prisma.io/docs/orm/overview/databases/mysql)を参照してください。

### Prisma schema を作成

Prisma schema の定義は、`schema.prisma` というファイルに作成していきます。こちらのファイルがない、またはセットアップしていない場合は、[プロジェクトのセットアップ](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-postgresql#create-project-setup)をご確認ください。
今回はチュートリアルと同様に以下の通り、ジェネレーター、データソース、３つのデータモデルを定義しました。

```
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   @db.VarChar(255)
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

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

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
  posts   Post[]
  profile Profile?
}
```

### マイグレーションを実行

Prisma schema を作成したので、[migrate dev](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-dev) コマンドでマイグレーションを実行します。

```zsh
% npx prisma migrate dev --name init
```

開発時は、上記のコマンドを実行して、マイグレーションファイルの作成とマイグレーションの適用を実行します。公式サイトでも解説されていますが、このコマンドは本番環境などの実働環境では利用しないでください。

https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production

実行後に生成されたマイグレーションファイルは、`prisma/migrations/[日付]_[name]/migration.sql` が作成されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/e4001776-ae27-3f90-6cd3-a72215d5f286.png)

`migration.sql` の中身は、DDL となっており、今回の場合は、テーブルを作成する SQL や、外部キーを追加する SQL となっていました。
また、データベースを確認してみたところ、テーブルや外部キー制約などが作成されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/05144076-06e3-be41-8a53-01cf509208bf.png)

データベースの設計や実装を新規に作成していくプロジェクトなどでは、Prisma schema を作成するところから始めるのが、シンプルで導入も簡単にできそうですね。

## 既存プロジェクトに組み込む

こちらのパターンではすでにデータベースは実装済みの場合に Prisma Migrate で管理したい場合です。新規プロジェクトの場合と同様に、公式のチュートリアルを参考に、`MySQL` を使って実際に試してみました。

https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project

### 既存のデータベースの状態

サンプルとして MySQL に以下のようなテーブルとデータが作成済みの状態となります。

```sql
CREATE TABLE orders
(
    order_id     INT AUTO_INCREMENT PRIMARY KEY,
    order_date   DATE           NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL
);

CREATE TABLE order_details
(
    order_detail_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id        INT            NOT NULL,
    product_id      INT            NOT NULL,
    quantity        INT            NOT NULL,
    unit_price      DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders (order_id)
);

INSERT INTO orders (order_date, total_amount)
VALUES ('2024-07-01', 150.00),
       ('2024-07-02', 200.00);

INSERT INTO order_details (order_id, product_id, quantity, unit_price)
VALUES (1, 101, 2, 50.00),
       (1, 102, 1, 50.00),
       (2, 103, 4, 50.00);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/7ccfc9dd-4765-30de-6ad2-887888f634a4.png)

### Prisma 関連ファイルの状態を確認

`prisma` ディレクトリ配下には、`schema.prisma` のみ配置されていて、ファイルの中身は以下のように、ジェネレーターとデータソースの定義のみとなります。

```
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}
```

### 実装済みのデータベースから Prisma schema を生成

[db pull](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#db-pull) コマンドを利用して、実装済みデータベースから Prisma schema を生成します。

```zsh
% npx prisma db pull
```

コマンドを実行すると schema.prisma ファイルがデータベースの実装情報に基づいたスキーマ情報で上書きされます。そのため、手動で編集した内容がある場合、編集内容が失われる可能性があるので注意が必要です。生成されるデータモデルの定義を確認したい場合は、schema.prisma ファイルを変更せずに画面上に表示することもできます（dry run のようなイメージ）。

```zsh
% npx prisma db pull --print
```

コマンドを実行すると以下のようなデータモデルが生成されていました。

```
model order_details {
  order_detail_id Int     @id @default(autoincrement())
  order_id        Int
  product_id      Int
  quantity        Int
  unit_price      Decimal @db.Decimal(10, 2)
  orders          orders  @relation(fields: [order_id], references: [order_id], onDelete: NoAction, onUpdate: NoAction, map: "order_details_ibfk_1")

  @@index([order_id], map: "order_id")
}

model orders {
  order_id      Int             @id @default(autoincrement())
  order_date    DateTime        @db.Date
  total_amount  Decimal         @db.Decimal(10, 2)
  order_details order_details[]
}
```

すでに実装されているデータベース定義を変更するのは難しいけど、プログラムで扱う命名規約に合わせて、マッピングしたい場合は、`@map` や `@@map` を利用することができます。詳しくは[公式ドキュメント](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/custom-model-and-field-names#using-map-and-map-to-rename-fields-and-models-in-the-prisma-client-api)を確認してください。
※`order_id` をプログラム上は `orderId` としたい場合など。

### baseline migration を作成

これまでの手順では、実装済みのデータベースから schema.prisma ファイルにデータモデルを生成しただけです。ですので、Prisma Migrate に既存のテーブルはマイグレーションで作成されたリソースだということを認識させる必要があります。（これによりマイグレーション実行時に、すでに存在するテーブルやフィールドを作成するのを防ぐことができる）

以下のコマンドを実行して、ディレクトリを作成します。ここで、`0_` とすることで、マイグレーションの適用順を最初にします。

```zsh
% mkdir -p prisma/migrations/0_init
```

続いて、[migrate diff](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-diff)コマンドでマイグレーション用のファイル(DDL)を生成します。

```zsh
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
```

### マイグレーションを適用済みにする

[migrate resolve](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-resolve) コマンドを実行して、実装済みのデータベース定義をすでにマイグレーション実行済みとなるようにします。

```zsh
% npx prisma migrate resolve --applied 0_init
```

コマンドを実行するとマイグレーション適用済みとなります。`_prisma_migrations` テーブルを確認することで適用済みとなっていることを確認できます。

これで`新規プロジェクトで使い始める`場合と同じ状態とすることができましたので、今後のマイグレーション情報の変更と適用は [migrate dev](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-dev) コマンドを利用して行うことができます。

## データベース定義を更新する

Prisma Migrate で管理しているデータベースを更新してみます。

### テーブルにカラムを追加する

`新規プロジェクトで使い始める` で作成した `User` テーブルに `jobTitle` カラムを追加してみます。
`schema.prisma` ファイルのデータモデルを以下の通り更新します。
```
model User {
  id       Int      @id @default(autoincrement())
  jobTitle String　　　　　　// 追加
  email    String   @unique
  name     String?
  posts    Post[]
  profile  Profile?
}
```

続いて [migrate dev](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-dev) コマンドでマイグレーションを実行します。

```zsh
% prisma migrate dev --name added_job_title
```

実行すると以下のようなエラーとなってしまいました。

```
 Step 0 Added the required column `jobTitle` to the `User` table without a default value. There are 1 rows in this table, it is not possible to execute this step.
```

このエラーはデータが登録済みのテーブルに対して、NULL を許容していないカラムを追加しようとすることで起こるエラーのようでした。このような場合は、生成されたマイグレーションファイルを編集して再度マイグレーションを実行することで解決することができます。

https://www.prisma.io/docs/orm/prisma-migrate/workflows/customizing-migrations#how-to-edit-a-migration-file

まずは、以下のコマンドを実行してマイグレーションファイルを作成します。
```zsh
% prisma migrate dev --name added_job_title --create-only
```

作成されたマイグレーションファイルを編集します。今回は、デフォルトバリューに空文字を設定してから、NOT NULL 制約を適用します。

```sql
ALTER TABLE `User` ADD COLUMN `jobTitle` VARCHAR(191) DEFAULT '';
ALTER TABLE `User` MODIFY COLUMN `jobTitle` VARCHAR(191) NOT NULL;
```

編集後、マイグレーションを実行します。

```
% prisma migrate dev --name added_job_title
```

これで NOT NULL 制約がついたカラムを作成することができました。データが登録済みのテーブルに変更を加える場合は、既存のデータを考慮する必要がありますね。

### カラム名を変更する

既存のカラム名を変更してみます。カラム名の変更時は、

- リネーム後のカラムを作成する
- 既存のカラムとそのカラムのデータを削除する
 
という動きとなるようです。リネーム後のカラムにそのままデータを移行したい場合は、マイグレーションファイルを編集して対応することができそうですので、実際にやってみます。

まずは、`schema.prisma` ファイルのデータモデルを更新します。

```
model User {
  id       Int      @id @default(autoincrement())
  jobTitle String
  email    String   @unique
  fullName String?  // name → fullName にリネーム
  posts    Post[]
  profile  Profile?
}
```

`create-only` オプションを使って、マイグレーションファイルを生成します。

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

以下のような確認メッセージが表示されるので `y` で `yes` とします。

```
You are about to drop the column `name` on the `User` table, which still contains 1 non-null values.

✔ Are you sure you want to create this migration? … yes
```

作成されたマイグレーションファイルを編集します。今回は、DROP, ADD される処理を、RENAME されるように修正します。

```sql
ALTER TABLE `User`
RENAME COLUMN `name` TO `fullName`;
```

修正後、再度マイグレーションを実行します。

```zsh
% npx prisma migrate dev --name rename-migration
```

`name` に設定していたデータを保持したまま、`fullName` にリネームすることができました。

## マイグレーションを動作環境に適用する

これまでは主に開発時に利用するコマンドを利用していましたが、本番環境やテスト環境などにマイグレーションを実行する場合は [migrate deploy](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-deploy) コマンドを利用します。 migrate deploy コマンドを実行することで保留中（未適用）のマイグレーションを実行することができます。

https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production#production-and-testing-environments

```zsh
% npx prisma migrate deploy
```

## 適用したマイグレーションをロールバックする

データベースのマイグレーションやアプリケーションのリリースを実行して動作確認してみると、アプリケーションが正しく動かないので、アプリケーションのロールバックが必要になることもありますよね。（このような状態は誰も望んでいないとは思いますが。。）
その際に実行したマイグレーションを元に戻す（ロールバック）方法について確認していきます。
ロールバック時の流れとしては、

- 開発環境でマイグレーションをもとに戻すクエリを生成
- 動作環境でマイグレーションを実行
- 動作環境で実行したマイグレーションをもとに戻すマイグレーションを実行

という流れになりますので、その実行方法を確認してみます。

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

https://www.prisma.io/docs/orm/prisma-migrate/workflows/patching-and-hotfixing

マイグレーションの状態は、`新規プロジェクトで使い始める` で最初に作成した状態から始めます。
マイグレーションで変更内容は、`UserDetail` というテーブルを追加し、`User` テーブルとリレーションを設定しました。

```
model User {
  id         Int         @id @default(autoincrement())
  email      String      @unique
  name       String?
  posts      Post[]
  profile    Profile?
  UserDetail UserDetail?  // 追加
}

// 追加
model UserDetail {
  id     Int    @id @default(autoincrement())
  detail String
  user   User   @relation(fields: [userId], references: [id])
  userId Int    @unique
}
```

この状態でマイグレーションをもとに戻すクエリを生成します（`down.sql`)

```zsh
% npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma \
--script > down.sql
```

`down.sql` には、以下のような SQL が生成されており、今回作成するオブジェクトを削除するクエリとなっているようでした。

```sql
-- DropForeignKey
ALTER TABLE `UserDetail` DROP FOREIGN KEY `UserDetail_userId_fkey`;

-- DropTable
DROP TABLE `UserDetail`;
```

続いて、開発環境でマイグレーションを実行します。

```zsh
% npx prisma migrate dev --name add_user_detail
```

これでマイグレーションファイルの生成が完了しましたので、管理しやすいように先ほど生成した `down.sql` をマイグレーションファイルと同じディレクトリにコピーしておきます。これで準備完了です。

```zsh
prisma
├── migrations
│   ├── 20240722093810_init
│   │   └── migration.sql
│   ├── 20240725232814_add_user_detail
│   │   ├── down.sql  // ここにコピー
│   │   └── migration.sql
│   └── migration_lock.toml
└── schema.prisma
```

ある実行環境で [migrate deploy](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-deploy) コマンド などでマイグレーションを実行してマイグレーションを適用したとします。このマイグレーションをロールバックする（戻したい）場合の手順を実行していきます。

[db execute](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#db-execute) コマンドを実行して、データベースに適用された変更を取り消します。

```zsh
% npx prisma db execute --file ./prisma/migrations/[マイグレーションで生成されたディレクトリ名]/down.sql --schema prisma/schema.prisma 
```

ここまでで、実行環境で動作するデータベースの定義は、マイグレーションの適用前に戻すことができているので、焦らなくても大丈夫です！
現在の状態は、稼働しているデータベースの状態とマイグレーションの状態に差異がある（`schema drift`している）状態となりますので、それを解消します。
まずは、開発環境を実行環境と同様の状態とするために、`db execute`（上記と同様）コマンドと `prisma.schema` ファイルをデータベースと同期するために `db pull` コマンドを実行します。

```zsh
% npx prisma db execute --file ./prisma/migrations/[マイグレーションで生成されたディレクトリ名]/down.sql --schema prisma/schema.prisma 
% npx prisma db pull
```

この状態でマイグレーションをロールバックするマイグレーションを作成します。

```zsh
% npx prisma migrate dev --name add_user_detail_roll_back
```

生成されたマイグレーションの SQL を確認すると、最初に作成した `down.sql` と同様の SQL が生成されていることを確認できました。
このマイグレーションは、実行環境のデータベースにはすでに適用済みのマイグレーションなので、すでに適用済みということを実行環境に適用しておきます。

```zsh
% npx prisma migrate resolve --applied "20240727022602_add_user_detail_roll_back"
```

これでデータベースの状態とマイグレーションの状態の同期をとることができました。

### Prisma Migrate を利用したロールバックについて

実際にマイグレーションした内容をロールバックしてみましたが、変更した内容や既存のデータの状態など、状況に応じて考慮するべき点が多く、素早く対応を求められる場面では、手順や対応内容が複雑に感じました。
Prisma Migrate でもロールバックすることはできそうですが、他の手法（例えば データベースのスナップショットから復元させるなど）でロールバックすることもできるので、どの方法が合っているかを見極めて、選択するのが良いのかと思います。

また、最初ドキュメントを読んでいたときに、[migrate resolve](https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-resolve) コマンドの `--rolled-back` オプションをつけて実行すれば、ロールバックされるのかなぁと思っていたのですが、これは今回の用途とは違い、マイグレーションが失敗した場合に利用するオプションでしたので、ご注意ください。（マイグレーションが成功したけど戻したい場合に利用するコマンドではない）