はじめに
本来であれば、1つのDBに対してORMは1つに統一するべきです。
しかし詳細は割愛しますが、RailsとHono(PrismaをORMとして使用するWebフレームワーク)が同じDBを触らざるを得ない状況になってしまいました。同じ境遇のエンジニアの方に向けて、その中でのベストなワークアラウンドを紹介します。
なお、この運用が理想的でないことは重々承知しています。最終的にはPrismaに一本化することを目指していますが、過渡期の対応としてこの記事が参考になれば幸いです。
この記事では _prisma_migrations テーブルの仕組みから prisma migrate resolve を使ったスキップ処理の実装、CI運用パターンまでを紹介します。RailsとPrismaが同一DBを参照している状況のエンジニアの方を対象にしています。
目次
| # | タイトル |
|---|---|
| 1. |
_prisma_migrations テーブルの仕組み |
| 2. | ベースラインで解決を試みる |
| 3. | なぜ migrate deploy がエラーになるのか |
| 4. |
prisma migrate resolve コマンドを理解する |
| 5. | 毎回 --applied するとエラーになる問題を解決する |
| 6. | CIへの組み込み |
| 7. | 運用フロー |
| 8. | まとめ |
_prisma_migrations テーブルの仕組み
まず前提知識として、Prismaがマイグレーションをどう管理しているかを理解する必要があります。
prisma migrate を実行すると、DBに _prisma_migrations というテーブルが自動生成されます。このテーブルに各マイグレーションの実行履歴が記録されており、Prismaはこれを見て「どのマイグレーションが適用済みか」を判断しています。
テーブルの主なカラム
| カラム名 | 内容 |
|---|---|
migration_name |
マイグレーションファイル名(タイムスタンプ+名前) |
finished_at |
正常完了した日時(NULLなら未完了) |
rolled_back_at |
ロールバックされた日時 |
logs |
エラーログ(失敗時に記録される) |
checksum |
マイグレーションSQLのSHA256ハッシュ |
ステータスの判定ロジック
| 状態 | finished_at |
rolled_back_at |
logs |
|---|---|---|---|
| 適用済み(成功) | 値あり | NULL | NULL |
| 失敗中 | NULL | NULL | エラーあり |
| ロールバック済み | NULL | 値あり | エラーあり |
| 未適用 | NULL | NULL | NULL |
この仕組みを理解しておくことが、後述の解決策のポイントになります。
ベースラインで解決を試みる
Prismaを既存DBに後から導入する場合、公式が推奨しているのがベースラインという方法です。
# 現在のDBの状態をマイグレーションファイルとして生成
mkdir -p prisma/migrations/0_init
npx prisma migrate diff \
--from-empty \
--to-schema-datamodel prisma/schema.prisma \
--script > prisma/migrations/0_init/migration.sql
# 「このマイグレーションは既に適用済み」としてマーク
npx prisma migrate resolve --applied "0_init"
これにより「既存のDBは全て適用済み」として扱われ、以降の差分だけをPrismaで管理できるようになります。
最初はこれで解決しました。
しかしその後、Rails側の開発が再開されました。Railsが定期的に新しいマイグレーションを追加し続ける状況になり、ベースライン(一回きりの解決策)では対応できなくなってしまったのです。
なぜ migrate deploy がエラーになるのか
状況を整理するとこうなります。
- Railsのマイグレーション → Railsが直接DBに適用済み → Prismaはスキップしたい
- Hono(Prisma)のマイグレーション → Prismaがちゃんとデプロイしたい
この2種類のマイグレーションが prisma/migrations/ に混在するため、単純に prisma migrate deploy を叩くとRailsが適用済みの変更を二重実行しようとしてエラーになってしまいます。
prisma migrate resolve コマンドを理解する
ここで鍵になるのが prisma migrate resolve コマンドです。
# 「このマイグレーションは適用済み扱い」にする(SQLは実行しない)
npx prisma migrate resolve --applied "20240101000000_rails_add_users"
# 「このマイグレーションはロールバック済み扱い」にする
npx prisma migrate resolve --rolled-back "20240101000000_some_migration"
--applied を使うと、実際にSQLを実行せずに _prisma_migrations テーブルの finished_at だけを書き込みます。これにより migrate deploy がそのマイグレーションをスキップするようになります。
使えるのはこの2パターンのみ
| パターン | 条件 |
|---|---|
| 未適用 |
_prisma_migrations にレコード自体が存在しない |
| 失敗中 | レコードはあるが finished_at がNULL |
⚠️ 既に成功済み(
finished_atに値あり)のマイグレーションに対して実行するとエラーになります。
毎回 --applied するとエラーになる問題を解決する
CIで毎回Railsのマイグレーションに対して --applied を実行しようとすると、2回目以降は「既に適用済み」としてエラーになってしまいます。
_prisma_migrations テーブルから該当レコードをDELETEしてから --applied することも考えましたが、これはPrisma的に非推奨で予期しない動作につながるリスクがあります。
解決策:ファイル名の命名規則 + SELECTで冪等に処理する
以下の2つの方針を組み合わせます。
-
命名規則の統一:Rails由来のマイグレーションファイルは全て
*_rails_imported_schemaという名前にする -
適用済みチェック:
finished_atをSELECTして、未適用のものだけ--appliedを実行する
これにより、新しいRailsマイグレーションを追加しても配列を手動メンテする必要がなく、何度CIを回しても同じ結果になる冪等な処理になります。
// scripts/skip-rails-migrations.ts
import { execSync } from "child_process"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
async function main() {
// `_rails_imported_schema` で終わるマイグレーションを全件取得
const rows = await prisma.$queryRaw<{ migration_name: string; finished_at: Date | null }[]>`
SELECT migration_name, finished_at FROM _prisma_migrations
WHERE migration_name LIKE '%_rails_imported_schema'
`
for (const row of rows) {
if (row.finished_at !== null) {
// 既に適用済み → スキップ
console.log(`Skip (already applied): ${row.migration_name}`)
} else {
// 未適用 → --applied でマーク(SQLは実行しない)
console.log(`Marking as applied: ${row.migration_name}`)
execSync(`npx prisma migrate resolve --applied "${row.migration_name}"`, {
stdio: "inherit",
})
}
}
await prisma.$disconnect()
}
main()
package.json にスクリプトを追加します:
{
"scripts": {
"migrate:skip-rails": "tsx scripts/skip-rails-migrations.ts",
"migrate:deploy": "pnpm migrate:skip-rails && prisma migrate deploy"
}
}
CIへの組み込み
migrate deploy の前にスクリプトを叩くだけです。
# GitHub Actions の例
- name: Deploy Prisma migrations
# migrate:skip-rails でRails由来をスキップ後、prisma migrate deploy を実行
run: pnpm migrate:deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
全体の流れはこうなります。
運用フロー
この設計により、日常の運用はこれだけです:
① Railsでマイグレーションを実行(DBが更新される)
② schema.prismaを別名で保存
cp prisma/schema.prisma prisma/schema.before.prisma
③ prisma db pull でschema.prismaをDBに合わせて更新
④ prisma migrate diff でマイグレーションファイルを生成
prisma migrate diff \
--from-schema-datamodel prisma/schema.before.prisma \
--to-schema-datamodel prisma/schema.prisma \
--script > prisma/migrations/YYYYMMDD_rails_imported_schema/migration.sql
※ このmigration.sqlは、Rails側で既にDBへ適用済みの変更をPrisma側のmigration履歴として
残すためのものです。CIでは `migrate resolve --applied` により適用済み扱いにするため、
通常このSQLは実行されません。
⑤ コミットするだけ。CIが自動でスキップ処理を行います。
まとめ
| やりたいこと | 方法 |
|---|---|
| Railsが適用済みの変更をPrismaにスキップさせたい | migrate resolve --applied |
| 毎回スキップ処理を冪等にしたい | SELECTで適用済みチェックしてからスキップ |
| Rails由来のマイグレーションを自動識別したい | ファイル名を *_rails_imported_schema に統一 |
| CIに組み込みたい | deployの前にスクリプトを実行 |
| 日常運用を最小化したい | コミットするだけでCIが処理 |
公式ドキュメントでは --applied の用途としてホットフィックスや初期ベースラインしか紹介されていませんが、複数ORMの共存という場面でも有効に使えることがわかりました。
同じ状況に陥った方の参考になれば幸いです。
株式会社シンシア
株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。