Prismaのマイグレーション管理に入門する

# はじめに
TypeScriptのORMツール[**Prisma**](https://www.prisma.io/)と戯れてみたものの、
「schema.prismaファイルと、migration.sqlと、どっちを参照してDBマイグレーションを実行しているんだ……？」
など、いまいち各操作でやってることがよくわからなかったので、自分の脳内整理がてら雑多にまとめてみる。

（ほんとうはCICD込みでどのようにマイグレーションの管理を行うべきかちょろっと考えてみたかったが、それはまたいずれ……）

::: note warn
本記事は、Prisma公式Docs……とりわけ概念周り（[Prisma Migrate / Understanding Prisma Migrate / Mental model](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/mental-model)あたりなど）の要約+よく使うコマンドチートシートみたいなものです。
:::


# （前提）Prismaの概要
## Prisma ORMとは
https://www.prisma.io/docs/orm/overview/introduction/what-is-prisma

Node.js または TypeScript バックエンドアプリケーション（サーバーレス アプリケーションやマイクロサービスを含む）で利用できるORM（Object Relational Mapper）。
MySQL、PostgresSQL、SQL ServerなどのRDBだけでなく、MongoDBなどにも対応している。
構成要素は以下3種。本記事では、**Prisma Migrate**を用いたマイグレーションについてまとめる。

- Prisma Client : クエリビルダー
- **Prisma Migrate : マイグレーションツール**
- Prisma Studio : データベース内のデータを表示および編集するための GUIツール

## Prismaスキーマとは
https://www.prisma.io/docs/orm/prisma-schema/overview

Prismaのセットアップを行う際には、**Prismaスキーマファイル**を用いる。
拡張子は`**.prisma`。中身はこんな感じ。

``` schema.prisma
// データソース設定
datasource db {
  provider = "sqlserver"
  // メインデータベース
  url      = env("DATABASE_URL")
  // シャドーデータベース
  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}

// クライアント設定
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["prismaSchemaFolder"]
  binaryTargets   = ["native", "debian-openssl-1.1.x"]
}

// データモデル設定
model Post {
  id        Int      @id @default(autoincrement())
  title     String   @db.NVarChar(255) @map("タイトル")
  createdAt DateTime @default(now()) @db.DateTime @map("作成日")
  content   String?  @db.NVarChar(max) @map("内容")
  published Boolean  @default(false) @map("公開")
  postUserId  Int   @map("投稿者ID")
  User      User     @relation(fields: [postUserId], references: [id], onDelete: NoAction, onUpdate: NoAction)

  @@map("投稿")
}

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

  @@map("プロフィール")
}

model User {
  id      Int      @id @default(autoincrement())
  name    String?  @db.NVarChar(255) @map("名前")
  email   String   @unique @db.VarChar(255) @map("メールアドレス")
  Post    Post[]
  Profile Profile?

  @@map("ユーザー")
}

```

**データソース**
Prisma ORMが接続するデータソースの詳細を指定する。
（PostgreSQL、SQL ServerなどのDB種別、利用するDBの接続先URLなど）

**ジェネレーター**
データモデル定義に基づいて生成するクライアントを指定する。
「Prisma Client」は必ず指定する。そのほかサードパーティー製クライアントを指定可能。
Prisma Client の詳細は割愛するが、ざっくり言うとアプリケーションからデータベースに接続し、クエリを実行できるようにするクライアントオブジェクト。

**データモデル**
アプリケーションで用いるデータモデル（Prisma モデル）を定義し、DBテーブルとマッピングする。
上の`schema.prisma`の例だと、SQL Serverのテーブル「投稿」「プロフィール」「ユーザー」の3種と、Prismaモデル「Post」「Profile」「User」がマッピングしている。


【memo】
とてもとても余談だが、SQL Serverは日本語名でテーブル名/カラム名を定義できる。知らなかった。
Prismaモデルは日本語名はNG。



# Prisma Migrateの概要
https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/overview

**Prisma Migrate** は、前述のPrismaスキーマと実データベーススキーマを同期させ、DBの既存データを維持しながら随時マイグレーションを行うことができる。

Prismaスキーマを用いて宣言的にマイグレーションSQLを生成するが、そのSQLファイルを個別カスタマイズすることも可能。そのため公式Docsでは、「ハイブリッドデータベース スキーマ移行ツールと見なすことができる」「宣言型要素と命令型要素の両方を備えている」と謳っている……。


https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/mental-model

上記の公式Docs「メンタルモデル」を参考に、もう少し掘り下げて説明してみる。
ここでは、以下2種のマイグレーション手法を上げている。

**Model/Entity-first migration**
コード上でデータベース スキーマの構造を定義し、マイグレーションSQLを生成する。
（→コードで定義したModel/Entityが正となる）

**Database-first migration**
データベースの構造を定義し、SQL を使用してそれをデータベースに適用。次に、データベースをイントロスペクトして、データベースの構造を記述するコードを生成し、アプリケーションとデータベース スキーマを同期。
（→データベースのスキーマ構造が正となる）

Prisma Migrateがサポートしているのは前者の**Model/Entity-first migration**。
Prisma Modelを元に、マイグレーションSQLを生成し、DBスキーマに反映させる。

（※ただし、後者の**Database-first migration**も、Prismaの機能を用いれば実現可能なので、後ほど軽く説明する）

# Prisma Migrateによるマイグレーションの流れ
DBのマイグレーションは、「ローカル環境」あるいは「本番環境・ステージング環境など」で実行されるが、Prisma Migrate では、前者と後者で用いるコマンドが異なるのが特徴。
基本的に、「ローカル環境のマイグレ用コマンドで生成したマイグレーションファイル」を、本番環境のマイグレーションに使用する……という流れになる。

## ローカル環境で実行するコマンド

### prisma migrate dev
Prismaスキーマを元に、必要に応じてマイグレーションSQLを作成し、DBに適用する。
（＋で最新のPrismaスキーマモデル定義を元にPrisma Clientを生成したり、Seedデータを初期投入したりする。この辺の詳細は本記事では割愛……）

```shell
# 基本形
prisma migrate dev

# マイグレーション名を指定する
prisma migrate dev --name <任意のマイグレーション名>

# マイグレーションのSQLの生成のみを行う
prisma migrate dev --name <任意のマイグレーション名> --create-only
```

`prisma migrate dev`コマンドは、以下4つの状態を用いてデータベースの状態を追跡している。

- Prismaスキーマ
- マイグレーション履歴（migrationsディレクトリ配下で管理）
- 移行テーブル（`_prisma_migrations`テーブル。後述）
- データベーススキーマ

`prisma migrate dev`実行時には、

- Prismaスキーマ情報とデータベーススキーマの状態に差異はあるか（**＝DB更新スクリプトを新規で生成する必要があるか否か**）
- `_prisma_migrations`で「適用済み」となっているマイグレーションが、マイグレーション履歴（migrationsディレクトリ配下で管理）に存在しているか（**＝整合性が取れているか**）
- `_prisma_migrations`とマイグレーション履歴を元に追跡した最新のDBマイグレーションと、データベーススキーマの状態に相違がないか（**＝整合性が取れているか**）

……といった具合で、情報の整合性と新規マイグレーションSQL作成の必要性を確認する。
整合性が取れている場合は、そのままマイグレーションの実行を進める。

まず、データベーススキーマに未適用のマイグレーション履歴が存在する場合は、それをデータベースに適用する（下の例だと、`20241218121233_second`が該当。ターミナルに`Applying migration 20241218121233_second`というメッセージが表示されている）

それから、新しいマイグレーションSQLが生成された場合は、それもデータベースに適用する（下の例だと、`20241218122741_third`が該当）

【src/prisma配下の構成例（コマンド実行前）】
```md
.
├── generated
├── migrations
│   ├── 20241202114138_first　←適用済み
│   │   └── migration.sql
│   ├── 20241218121233_second　←未適用
│   │   └── migration.sql
│   └── migration_lock.toml
├── schema.prisma　←更新あり
└── seed.ts
```
【例：`third`という名前のマイグレーションファイルを生成し、DBマイグレーションを実行する】
```shell
$ npx prisma migrate dev --name third
Environment variables loaded from prisma/.env
Prisma schema loaded from prisma/schema
Datasource "db": SQL Server database

Applying migration `20241218121233_second`

The following migration(s) have been applied:

migrations/
  └─ 20241202114138_first/
    └─ migration.sql
  └─ 20241218121233_second/
    └─ migration.sql
✔ Enter a name for the new migration: … 
Applying migration `20241218122741_third`


The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20241218122741_third/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (v6.0.0) to ./node_modules/@prisma/client in 118ms
✔ Generated Zod Prisma Types to ./prisma/generated/zod in 36ms
```

ログにある通り、`migrations`ディレクトリ内にマイグレーションSQLが生成されている。
ディレクトリ名は`日時_指定したマイグレーション名`の形式。

【src/prisma配下の構成例(コマンド実行後)】
```md
.
├── generated
├── migrations
│   ├── 20241202114138_first
│   │   └── migration.sql
│   ├── 20241218121233_second
│   │   └── migration.sql
│   ├── 20241218122741_third　←NEW！
│   │   └── migration.sql
│   └── migration_lock.toml
├── schema.prisma
└── seed.ts
```

また、データベースを覗くと`_prisma_migrations`なるテーブルが自動で作られている。
中身はこんな感じ。マイグレーション名とマイグレーション完了日時、（ロールバックした場合）ロールバック日時などがデータとして入ってくる。
（※ロールバック関連は後述）
![_prisma_migrations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/00430e60-f511-da22-a068-90c9c043325c.png)


なお、Prismaスキーマとマイグレーション履歴、`_prisma_migrations`テーブル、データベーススキーマの整合性が取れていない場合、下記のようなメッセージが出てくる。一度DBスキーマをリセットする形になるので、データが失われる。

「SQLでDB定義を直接修正した場合」「（後述の）`prisma db push`コマンドを利用した場合」などに起こり得るので要注意。

```shell
$ npx prisma migrate dev --name add_test_table                          
Environment variables loaded from prisma/.env
Prisma schema loaded from prisma/schema
Datasource "db": SQL Server database

- Drift detected: Your database schema is not in sync with your migration history.

The following is a summary of the differences between the expected database schema given your migrations files, and the actual schema of the database.

It should be understood as the set of changes to get from the expected schema to the actual schema.

[+] Added tables
  - プロフィール
  - ユーザー
  - 投稿

[-] Removed tables
  - Post
  - Profile
  - User

[*] Changed the `Post` table
  [-] Removed foreign key on columns (postUserId)

[*] Changed the `Profile` table
  [-] Removed foreign key on columns (userId)

[*] Changed the `プロフィール` table
  [+] Added foreign key on columns (ユーザーID)

[*] Changed the `投稿` table
  [+] Added foreign key on columns (投稿者ID)

- The migrations recorded in the database diverge from the local migrations directory. Last common migration: `20241218121233_test`. Migrations applied to the database but absent from the migrations directory are: 20241218122741_fix

? We need to reset the database schema
Do you want to continue? All data will be lost. › (y/N)
```

***
### prisma migrate diff
`prisma migrate diff`は、2つのデータソース（DBスキーマ、Prismaスキーマ……etc）を比較し、最初のソースを2つ目のソースの状態に移行するためのスクリプトを生成する。

よく使うケースは、マイグレーションのダウングレードSQLを生成する場合。
Prismaでは、ダウングレードSQLは自動で生成はされない。
ただ、`prisma migrate dev`実行後に、「シャドーデータベースとメインデータベースの定義の差分をもとに、ダウングレードSQLを生成する」ことはできる。この際に用いるのが`prisma migrate diff`コマンド。

##### シャドーデータベースとは
https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/shadow-database

Prismaでは、実際に使用するメインデータベースとは別に、**シャドーデータベース**という、直近の`prisma migrate dev`を実行する前の状態を残しておくデータベースを自動で作成する。

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

このシャドーデータベースとメインデータベースの状態を比較することで、ダウングレードSQLを生成することができる。

以下はコマンド実行例。
```shell
prisma migrate diff --from-url $DATABASE_URL --to-url $SHADOW_DATABASE_URL --script > down.sql
```

:::note warn
シャドーデータベースを使わずともdown.sqlを生成すること自体は可能。（参考：下記URL）
https://www.prisma.io/docs/orm/prisma-migrate/workflows/generating-down-migrations

だが、データベースのスキーマが最新でないとおかしな内容のスクリプトが生成されてしまうなど、あまり堅牢でないため、個人的にはシャドーデータベースとメインデータベースを比較する構成の方が良いと考えている……。
:::

***
### prisma db push
これはスキーマのプロトタイプを作成するためのコマンド。
Prismaスキーマをデータベーススキーマに反映させる点は、`prisma migrate dev`と同じ。
しかし、

- 移行履歴（migration.sql）を生成しない
- `_prisma_migrations`テーブルを更新しない＆参照しない
- Prismaスキーマとデータベーススキーマだけを参照し、必要に応じてデータベーススキーマを更新する

といった大きな違いがある。

イメージ的にはこんな感じ。
- `prisma migrate dev` -> migration.sql を用いてデータベーススキーマを更新、ちゃんと移行履歴を管理するし、整合性もチェックする
- `prisma db push` -> schema.prisma で定義したPrismaモデルを用いてデータベーススキーマを更新、移行履歴管理はしない

チーム開発では、大抵の場合は`prisma migrate dev`を用いて履歴管理する方がベターかと思うが、
例えば「立ち上げ初期でテーブル定義もまだ確定ではなく、トライアンドエラーで修正したいので、さくっとスキーマのプロトタイプを作っておきたい」というような場合には有用。（後で修正が発生しても、マイグレーションファイルが残らないので、マイグレーション履歴が汚くならない）

***
### prisma db execute
単純にSQLを実行するコマンド。
Prisma 移行テーブル`_prisma_migrations`に変更はかからない。本当にSQL実行するだけ。

DBに対し普通にSQLを流し込むのとほぼ同等だが、このコマンドを用いるメリットとしては下記を体感している。

- prisma.schema のデータソースセクションで設定したメインデータベースURLをそのまま用いることができる
- `prisma migrate diff` コマンドで生成した `down.sql` など、`prisma`ディレクトリ配下で管理しているSQLファイルを簡単に実行することができる

実行方法は以下のような具合。
下の例は「prisma.schema のデータソースセクションで設定したメインデータベースURL」に対し、指定したSQLファイルのスクリプトを実行している。
```shell
# `./script.sql`を、schema.prismaのデータソースとして指定されているメインデータベースに対し実行する
prisma db execute --file ./script.sql --schema schema.prisma
```
そのほか、「引数`--url`に設定したデータベースURL」に対しコマンドを実行することもできる。
また、SQLスクリプトはターミナル入力で指定することもできる。（下の例参照）
```shell
# ターミナル入力から SQL スクリプトを取得し、環境変数で指定されたデータベースURLに対し実行する
echo 'TRUNCATE TABLE dev;' | prisma db execute --stdin --url="$DATABASE_URL"
```

***
### prisma migrate resolve
`_prisma_migrations`テーブルを更新し、指定したマイグレーションを「DBに適用済み」あるいは「ロールバック済み」であるとマークするコマンド。
更新するのは`_prisma_migrations`テーブルのみなので、ロールバック対応やマイグレーション適用対応は別のコマンドを用いて実行する必要がある。

```shell
# 「ロールバック済み」としてマーク
prisma migrate resolve --rolled-back <マイグレ名>

# 「適用済み」としてマーク
prisma migrate resolve --applied <マイグレ名>
```
#### prisma migrate resolve --rolled-back <マイグレ名>
特定のマイグレーションを「ロールバック済み」であるとマークする際に用いる。

##### ロールバックの具体例
まず、`prisma migrate diff` コマンドで生成した down.sql を、`prisma db execute`コマンドで実行する。（以下）
```shell
prisma db execute --file ./down.sql --schema prisma/schema.prisma
```

ただ、このままだと`_prisma_migrations`テーブルの情報が更新されていない。
`_prisma_migrations`テーブルの`roll_backed_at`カラムに日時情報がINSERTされることで、「このマイグレーションはロールバック済みなので」とマークすることができるのだが、今はダウングレードSQLを実行しただけになっている。

以下コマンドでロールバックを適用する。
```
prisma migrate resolve --rolled-back <ロールバック対象のマイグレ名>
```
すると、`_prisma_migrations`テーブルの`roll_backed_at`カラムに日時情報がINSERTされる。


#### prisma migrate resolve --applied <マイグレ名>
Prisma Migrate導入前から存在していたDB定義をPrisma Migrateの管理下に置く（＝ベースラインを設定する）ときに用いる。


##### ベースライン設定とは
https://www.prisma.io/docs/orm/prisma-migrate/workflows/baselining

通常、Prisma Migrateは最初にマイグレーションを作成する際に、データベースの状態とスキーマの差分を基にマイグレーションファイルを生成する。しかし、既に運用中のデータベースがある場合、そのデータベースにはPrismaのマイグレーション履歴が存在しない。
（前述の通り、Prismaスキーマとマイグレーション履歴、`_prisma_migrations`テーブル、データベーススキーマの整合性が取れていないという判定になる。一度DBスキーマをリセットするよう促され、既存テーブルやデータが失われかねない）

この場合、「ベースライン設定」を使って、既存のデータベース状態を「基準」として設定し、その後のマイグレーションを追跡できるようにする。

##### ベースラインの流れ
(1) 既存のDB定義を元に、Prismaモデルを生成する。手作業で作成する、あるいは`prisma db pull`コマンド（後述）を実行する。
(2) ベースライン用のマイグレーションSQLを配置するディレクトリを作成する。今回は`migrations`配下に`0_init`を作ることとする。
(3) ベースライン用のマイグレーションSQLを生成する。この際`prisma migrate dev`は利用できないので、`prisma migrate diff`というコマンドを用いる。

```shell
npx prisma migrate diff \
--from-empty \
--to-schema-datamodel prisma/schema.prisma \
--script > prisma/migrations/0_init/migration.sql
```
(4) 以下のコマンドを実行する。指定したマイグレーションが既に適用されているものとして扱われ、Prismaはその状態をもとに今後のマイグレーションを管理する。
```shell
npx prisma migrate resolve --applied "20230101000000_init"
```

***
### prisma migrate reset
マイグレーションの履歴をリセットして、データベースを初期状態に戻すコマンド。開発中に試行錯誤を繰り返す際に有用。
データベースの状態を再構築するため、重要なデータが失われる可能性がある点に注意が必要。

```shell
prisma migrate reset
```

***
### prisma db pull
prisma db pullコマンドは、既存のデータベースからスキーマ情報を抽出して、Prismaのschema.prismaファイルに自動的に反映させるためのコマンド。
すでに存在するデータベースをPrismaで管理し始める場合に非常に便利。
また、データベースの定義をSQL等で直接変更した場合、変更をPrismaのスキーマに反映させるのも容易になる。

`prisma db pull`を実行した後、`prisma migrate dev --create-only`を実行すれば、マイグレーションSQLを生成することも可能。
**Database-first migration**をPrismaで実現することができる。

ただし、**Model/Entity-first migration**前提でテーブル作成を進める場合は、上記の手段だけだと `_prisma_migrations`テーブルに変更履歴が反映されないため不都合。


`prisma db pull`→`prisma migrate dev --create-only`……という手順は、あくまで**Database-first migration**で対応を進める場合にのみ有用である点に留意が必要。


ちなみに、`--print`というオプションもある。
このオプションをつけると、schema.prismaが更新されることはなく、ターミナルに情報が吐き出されるだけ。
自動生成内容を見直しながら、部分的にschema.prismaに反映させたい……といった場合に有用か。


## ステージング、テスト、本番環境で実行するコマンド

### prisma migrate deploy
ステージング、テスト、および本番環境に変更を展開するために使用される。このコマンドは、マイグレーションSQLファイルのみを実行する。モデルの取得に Prisma スキーマは使用しない。

`_prisma_migrations`テーブルを確認し、prisma/migrationsフォルダ内の未適用のマイグレーションのみを順番に適用していく。すでに適用されたマイグレーションは再適用されない。

### npx prisma db execute
前述。ロールバック用SQL実行コマンド。
```shell
prisma db execute --file ./down.sql --schema prisma/schema.prisma
```
### prisma migrate resolve --rolled-back
前述。ロールバック完了を`_prisma_migrations`テーブルに反映させるコマンド。
```
prisma migrate resolve --rolled-back <ロールバック対象のマイグレ名>
```


# おわりに（余談）
今回は主に開発環境のマイグレーションに使えるコマンドや概念をメインに取り上げた。
が、振り返ってみると、本記事の内容の半分程度は、Prisma公式Docsの「[Prisma Migrate / Understanding Prisma Migrate / Mental model](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/mental-model)」でカバーできてしまう模様。いわば、メンタルモデルの記事の要約+コマンドチートシートみたいなもの。

Prisma Migrate は特に、コマンド単体だけ見ていると「何がどうしてそうなったんだ？」がよくわからない部分が多かったため、このようなまとめ方になっている。公式を読みつつ文字起こししつつ……と脳内整理しながら、まずはツールの目的と概念からおさえることが大事だと再認識した。。。
そして私よ、闇雲に手を動かす前に公式を読もう（自戒）

まだまだ掘り下げて紹介したい＆調べたい要素も多々残っている。冒頭でも触れたが、本当はCICD込みでどのようにマイグレーションの管理を行うべきかも考えたい……。
改めてPrisma第2弾記事は執筆する所存也……。