原則上,對於一個資料庫,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理解這個機制,是後面解法的重點。
當 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 會報錯把情況整理一下會是這樣:
由於這兩種 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。
狀態條件未套用_prisma_migrations 中根本沒有這筆紀錄失敗中有紀錄,但 finished_at 為 NULL> ⚠️ 如果對已經成功完成(finished_at 已有值)的 migration 執行,會報錯。
--applied 會報錯的問題如果在 CI 中每次都對 Rails 的 migration 執行 --applied,那麼從第二次開始就會因為「已經套用過」而報錯。
也考慮過先從 _prisma_migrations 資料表刪掉對應紀錄再執行 --applied,但這在 Prisma 的角度不建議,還可能導致非預期行為。
結合以下兩個方針:
*_rails_imported_schemafinished_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>
只要在 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