18
3

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 migrate resolveを使ってRails+Prismaのマイグレーションを共存させる

18
Posted at

はじめに

本来であれば、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つの方針を組み合わせます。

  1. 命名規則の統一:Rails由来のマイグレーションファイルは全て *_rails_imported_schema という名前にする
  2. 適用済みチェック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人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。


参考

18
3
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
18
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?