はじめに
TypeScriptのORMツールPrismaと戯れてみたものの、
「schema.prismaファイルと、migration.sqlと、どっちを参照してDBマイグレーションを実行しているんだ……?」
など、いまいち各操作でやってることがよくわからなかったので、自分の脳内整理がてら雑多にまとめてみる。
(ほんとうはCICD込みでどのようにマイグレーションの管理を行うべきかちょろっと考えてみたかったが、それはまたいずれ……)
本記事は、Prisma公式Docs……とりわけ概念周り(Prisma Migrate / Understanding Prisma Migrate / Mental modelあたりなど)の要約+よく使うコマンドチートシートみたいなものです。
(前提)Prismaの概要
Prisma ORMとは
Node.js または TypeScript バックエンドアプリケーション(サーバーレス アプリケーションやマイクロサービスを含む)で利用できるORM(Object Relational Mapper)。
MySQL、PostgresSQL、SQL ServerなどのRDBだけでなく、MongoDBなどにも対応している。
構成要素は以下3種。本記事では、Prisma Migrateを用いたマイグレーションについてまとめる。
- Prisma Client : クエリビルダー
- Prisma Migrate : マイグレーションツール
- Prisma Studio : データベース内のデータを表示および編集するための GUIツール
Prismaスキーマとは
Prismaのセットアップを行う際には、Prismaスキーマファイルを用いる。
拡張子は**.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の概要
Prisma Migrate は、前述のPrismaスキーマと実データベーススキーマを同期させ、DBの既存データを維持しながら随時マイグレーションを行うことができる。
Prismaスキーマを用いて宣言的にマイグレーションSQLを生成するが、そのSQLファイルを個別カスタマイズすることも可能。そのため公式Docsでは、「ハイブリッドデータベース スキーマ移行ツールと見なすことができる」「宣言型要素と命令型要素の両方を備えている」と謳っている……。
上記の公式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データを初期投入したりする。この辺の詳細は本記事では割愛……)
# 基本形
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配下の構成例(コマンド実行前)】
.
├── generated
├── migrations
│ ├── 20241202114138_first ←適用済み
│ │ └── migration.sql
│ ├── 20241218121233_second ←未適用
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma ←更新あり
└── seed.ts
【例:third
という名前のマイグレーションファイルを生成し、DBマイグレーションを実行する】
$ 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配下の構成例(コマンド実行後)】
.
├── 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スキーマとマイグレーション履歴、_prisma_migrations
テーブル、データベーススキーマの整合性が取れていない場合、下記のようなメッセージが出てくる。一度DBスキーマをリセットする形になるので、データが失われる。
「SQLでDB定義を直接修正した場合」「(後述の)prisma db push
コマンドを利用した場合」などに起こり得るので要注意。
$ 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
コマンド。
シャドーデータベースとは
Prismaでは、実際に使用するメインデータベースとは別に、シャドーデータベースという、直近のprisma migrate dev
を実行する前の状態を残しておくデータベースを自動で作成する。
このシャドーデータベースとメインデータベースの状態を比較することで、ダウングレードSQLを生成することができる。
以下はコマンド実行例。
prisma migrate diff --from-url $DATABASE_URL --to-url $SHADOW_DATABASE_URL --script > down.sql
シャドーデータベースを使わずとも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ファイルのスクリプトを実行している。
# `./script.sql`を、schema.prismaのデータソースとして指定されているメインデータベースに対し実行する
prisma db execute --file ./script.sql --schema schema.prisma
そのほか、「引数--url
に設定したデータベースURL」に対しコマンドを実行することもできる。
また、SQLスクリプトはターミナル入力で指定することもできる。(下の例参照)
# ターミナル入力から SQL スクリプトを取得し、環境変数で指定されたデータベースURLに対し実行する
echo 'TRUNCATE TABLE dev;' | prisma db execute --stdin --url="$DATABASE_URL"
prisma migrate resolve
_prisma_migrations
テーブルを更新し、指定したマイグレーションを「DBに適用済み」あるいは「ロールバック済み」であるとマークするコマンド。
更新するのは_prisma_migrations
テーブルのみなので、ロールバック対応やマイグレーション適用対応は別のコマンドを用いて実行する必要がある。
# 「ロールバック済み」としてマーク
prisma migrate resolve --rolled-back <マイグレ名>
# 「適用済み」としてマーク
prisma migrate resolve --applied <マイグレ名>
prisma migrate resolve --rolled-back <マイグレ名>
特定のマイグレーションを「ロールバック済み」であるとマークする際に用いる。
ロールバックの具体例
まず、prisma migrate diff
コマンドで生成した down.sql を、prisma db execute
コマンドで実行する。(以下)
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の管理下に置く(=ベースラインを設定する)ときに用いる。
ベースライン設定とは
通常、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
というコマンドを用いる。
npx prisma migrate diff \
--from-empty \
--to-schema-datamodel prisma/schema.prisma \
--script > prisma/migrations/0_init/migration.sql
(4) 以下のコマンドを実行する。指定したマイグレーションが既に適用されているものとして扱われ、Prismaはその状態をもとに今後のマイグレーションを管理する。
npx prisma migrate resolve --applied "20230101000000_init"
prisma migrate reset
マイグレーションの履歴をリセットして、データベースを初期状態に戻すコマンド。開発中に試行錯誤を繰り返す際に有用。
データベースの状態を再構築するため、重要なデータが失われる可能性がある点に注意が必要。
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で実現することができる。
ステージング、テスト、本番環境で実行するコマンド
prisma migrate deploy
ステージング、テスト、および本番環境に変更を展開するために使用される。このコマンドは、マイグレーションSQLファイルのみを実行する。モデルの取得に Prisma スキーマは使用しない。
_prisma_migrations
テーブルを確認し、prisma/migrationsフォルダ内の未適用のマイグレーションのみを順番に適用していく。すでに適用されたマイグレーションは再適用されない。
npx prisma db execute
前述。ロールバック用SQL実行コマンド。
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」でカバーできてしまう模様。いわば、メンタルモデルの記事の要約+コマンドチートシートみたいなもの。
Prisma Migrate は特に、コマンド単体だけ見ていると「何がどうしてそうなったんだ?」がよくわからない部分が多かったため、このようなまとめ方になっている。公式を読みつつ文字起こししつつ……と脳内整理しながら、まずはツールの目的と概念からおさえることが大事だと再認識した。。。
そして私よ、闇雲に手を動かす前に公式を読もう(自戒)
まだまだ掘り下げて紹介したい&調べたい要素も多々残っている。冒頭でも触れたが、本当はCICD込みでどのようにマイグレーションの管理を行うべきかも考えたい……。
改めてPrisma第2弾記事は執筆する所存也……。