8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-18

はじめに

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。中身はこんな感じ。

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の概要

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_migrations.png

なお、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弾記事は執筆する所存也……。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?