前言

原則上,對於一個資料庫,ORM 最好統一只使用一種。

不過先略過細節,Rails 和 Hono(使用 Prisma 作為 ORM 的 Web 框架)因為某些原因,不得不同時存取同一個資料庫。本文是寫給遇到同樣情境的工程師,介紹其中最好的 workaround。

另外,我也很清楚這種運作方式並非理想。最終目標仍然是統一到 Prisma,但希望這篇文章能作為過渡期的作法參考。

本文會從 _prisma_migrations 資料表的機制開始,介紹如何透過 prisma migrate resolve 實作跳過處理,以及 CI 的運用模式。對象是 Rails 與 Prisma 共用同一個 DB 的工程師。


目錄

#標題1._prisma_migrations 資料表的機制2.嘗試用 baseline 解決3.為什麼 migrate deploy 會報錯4.理解 prisma migrate resolve 指令5.解決每次都 --applied 會報錯的問題6.整合到 CI7.運作流程8.總結---

_prisma_migrations 資料表的機制

首先,作為前提知識,需要先理解 Prisma 是如何管理 migration 的。

執行 prisma migrate 時,資料庫會自動建立一個名為 _prisma_migrations 的資料表。這張表會記錄各個 migration 的執行歷程,Prisma 會根據它來判斷「哪些 migration 已經套用」。

資料表的主要欄位

欄位名內容migration_name migration 檔名(時間戳記 + 名稱)finished_at 正常完成的時間(若為 NULL 代表未完成)rolled_back_at rollback 的時間logs 錯誤紀錄(失敗時寫入)checksum migration SQL 的 SHA256 雜湊值### 狀態判定邏輯

狀態finished_at rolled_back_at logs已套用(成功)有值NULLNULL失敗中NULLNULL有錯誤已 rollbackNULL有值有錯誤未套用NULLNULLNULL理解這個機制,是後面解法的重點。


嘗試用 baseline 解決

當 Prisma 後來才導入既有資料庫時,官方建議使用的方法是 baseline

<span># 將目前 DB 的狀態生成為 migration 檔案</span>
<span>mkdir</span> <span>-p</span> prisma/migrations/0_init
npx prisma migrate diff <span>\</span>
  <span>--from-empty</span> <span>\</span>
  <span>--to-schema-datamodel</span> prisma/schema.prisma <span>\</span>
  <span>--script</span> <span>></span> prisma/migrations/0_init/migration.sql

<span># 標記為「這個 migration 已經套用過」</span>
npx prisma migrate resolve <span>--applied</span> <span>"0_init"</span>

這樣一來,系統就會把「既有資料庫全部視為已套用」,之後只需由 Prisma 管理差異部分即可。

一開始我就是這樣解決的。

但後來 Rails 端的開發又重新開始了。Rails 會定期持續新增新的 migration,在這種情況下,baseline(一次性的解法)就無法應對了。


為什麼 migrate deploy 會報錯

把情況整理一下會是這樣:

  • Rails 的 migration → 已由 Rails 直接套用到資料庫 → Prisma 希望跳過
  • Hono(Prisma)的 migration → Prisma 希望正常部署

由於這兩種 migration 會混在 prisma/migrations/ 裡,直接執行 prisma migrate deploy 時,Prisma 會嘗試重複執行 Rails 已經套用過的變更,因而報錯。


prisma migrate resolve 指令的理解

這時候的關鍵就是 prisma migrate resolve

<span># 將這個 migration 視為「已套用」(不執行 SQL)</span>
npx prisma migrate resolve <span>--applied</span> <span>"20240101000000_rails_add_users"</span>

<span># 將這個 migration 視為「已 rollback」</span>
npx prisma migrate resolve <span>--rolled-back</span> <span>"20240101000000_some_migration"</span>

使用 --applied 時,會不實際執行 SQL,只把 _prisma_migrations 資料表中的 finished_at 寫入。如此一來,migrate deploy 就會跳過該 migration。

只能用在這 2 種情況

狀態條件未套用_prisma_migrations 中根本沒有這筆紀錄失敗中有紀錄,但 finished_at 為 NULL> ⚠️ 如果對已經成功完成(finished_at 已有值)的 migration 執行,會報錯。


解決每次都 --applied 會報錯的問題

如果在 CI 中每次都對 Rails 的 migration 執行 --applied,那麼從第二次開始就會因為「已經套用過」而報錯

也考慮過先從 _prisma_migrations 資料表刪掉對應紀錄再執行 --applied,但這在 Prisma 的角度不建議,還可能導致非預期行為。

解決方案:檔名命名規則 + 透過 SELECT 做冪等處理

結合以下兩個方針:

  1. 統一命名規則:所有 Rails 來源的 migration 檔名都命名為 *_rails_imported_schema
  2. 檢查是否已套用:先用 SELECT 查 finished_at,只對未套用的項目執行 --applied

如此一來,即使新增新的 Rails migration,也不需要手動維護陣列,而且每次 CI 執行的結果都一致,成為冪等處理。

<span>// scripts/skip-rails-migrations.ts</span>
<span>import</span> <span>{</span> <span>execSync</span> <span>}</span> <span>from</span> <span>"</span><span>child_process</span><span>"</span>
<span>import</span> <span>{</span> <span>PrismaClient</span> <span>}</span> <span>from</span> <span>"</span><span>@prisma/client</span><span>"</span>

<span>const</span> <span>prisma</span> <span>=</span> <span>new</span> <span>PrismaClient</span><span>()</span>

<span>async</span> <span>function</span> <span>main</span><span>()</span> <span>{</span>
  <span>// 取得所有以 `_rails_imported_schema` 結尾的 migration</span>
  <span>const</span> <span>rows</span> <span>=</span> <span>await</span> <span>prisma</span><span>.</span><span>$queryRaw</span><span><</span><span>{</span> <span>migration_name</span><span>:</span> <span>string</span><span>;</span> <span>finished_at</span><span>:</span> <span>Date</span> <span>|</span> <span>null</span> <span>}[]</span><span>></span><span>`
    SELECT migration_name, finished_at FROM _prisma_migrations
    WHERE migration_name LIKE '%_rails_imported_schema'
  `</span>

  <span>for </span><span>(</span><span>const</span> <span>row</span> <span>of</span> <span>rows</span><span>)</span> <span>{</span>
    <span>if </span><span>(</span><span>row</span><span>.</span><span>finished_at</span> <span>!==</span> <span>null</span><span>)</span> <span>{</span>
      <span>// 已經套用過 → 跳過</span>
      <span>console</span><span>.</span><span>log</span><span>(</span><span>`Skip (already applied): </span><span>${</span><span>row</span><span>.</span><span>migration_name</span><span>}</span><span>`</span><span>)</span>
    <span>}</span> <span>else</span> <span>{</span>
      <span>// 尚未套用 → 用 --applied 標記(不執行 SQL)</span>
      <span>console</span><span>.</span><span>log</span><span>(</span><span>`Marking as applied: </span><span>${</span><span>row</span><span>.</span><span>migration_name</span><span>}</span><span>`</span><span>)</span>
      <span>execSync</span><span>(</span><span>`npx prisma migrate resolve --applied "</span><span>${</span><span>row</span><span>.</span><span>migration_name</span><span>}</span><span>"`</span><span>,</span> <span>{</span>
        <span>stdio</span><span>:</span> <span>"</span><span>inherit</span><span>"</span><span><span>,</span>
      <span>})</span>
    <span>}</span>
  <span>}</span>

  <span>await</span> <span>prisma</span><span>.</span><span>$disconnect</span><span>()</span>
<span>}</span>

<span>main</span><span>()</span>

package.json 裡加上 script:

<span>{</span><span>
  </span><span>"scripts"</span><span>:</span><span> </span><span>{</span><span>
    </span><span>"migrate:skip-rails"</span><span>:</span><span> </span><span>"tsx scripts/skip-rails-migrations.ts"</span><span>,</span><span>
    </span><span>"migrate:deploy"</span><span>:</span><span> </span><span>"pnpm migrate:skip-rails && prisma migrate deploy"</span><span>
  </span><span>}</span><span>
</span><span>}</span><span>
</span>

整合到 CI

只要在 migrate deploy 前面執行這個 script 即可。

<span># GitHub Actions 範例</span>
<span>-</span> <span>name</span><span>:</span> <span>Deploy Prisma migrations</span>
  <span># 先用 migrate:skip-rails 跳過 Rails 來源的 migration,再執行 prisma migrate deploy</span>
  <span>run</span><span>:</span> <span>pnpm migrate:deploy</span>
  <span>env</span><span>:</span>
    <span>DATABASE_URL</span><span>:</span> <span>${{ secrets.DATABASE_URL }}</span>

整體流程會變成這樣。


運作流程

有了這個設計,平常的運作只需要這些步驟:

① 在 Rails 中執行 migration(資料庫會被更新)

② 另存 schema.prisma
   cp prisma/schema.prisma prisma/schema.before.prisma

③ 使用 prisma db pull,讓 schema.prisma 與資料庫同步更新

④ 用 prisma migrate diff 產生 migration 檔案
   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。

⑤ 只要 commit 即可,CI 會自動進行跳過處理。

總結

想做的事方法想讓 Rails 已套用的變更在 Prisma 端被跳過migrate resolve --applied想讓每次跳過處理都具冪等性先用 SELECT 檢查是否已套用,再執行 skip想自動辨識 Rails 來源的 migration將檔名統一為 *_rails_imported_schema想整合到 CI在 deploy 前執行 script想把日常操作降到最少只要 commit,CI 自動處理官方文件中對 --applied 的用途,多半只提到 hotfix 或初始 baseline,但實際上也能有效用在多個 ORM 共存的情境。

希望這篇文章能對遇到同樣問題的人有所幫助。

株式会社シンシア

株式会社xincere 有招募沒有實務經驗的工程師,以及學生工程師實習生,一起工作。
※ 關於シンシア的工作方式可參考這裡

在シンシア,每年大約有 100 位沒有實務經驗的人投遞履歷並參加技術面試。
透過這些經驗,我們在這裡介紹希望沒有實務經驗者特別具備的技術能力(語法)。


參考


原文出處:https://qiita.com/tatsuya582/items/fc215131805b7523c20b


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝3   💬3   ❤️1
198
🥈
我愛JS
💬2  
7
🥉
Gigi
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登