ASP.NET Core Migration 實踐

本篇整理 ASP.NET Core 搭配 Entity Framework Core 的 migration 實踐。

Migration 是什麼?

在寫 database migration 之前,我們得先認識一下 migration 是什麼。

在我們後端專案開發時,可能會因為新功能的開發或功能異動導致資料庫的 schema 產生變化。舉個具體的例子,假設某天網站的使用者資訊突然要增加其他內容,我們可能會直接在新版本程式上線前,手動用 SQL 指令調整 table 格式。在這個案例下,可能會發生幾種問題。首先,我們在執行 SQL 指令前,若沒有做過相應的驗證,可能會讓指令執行完的結果不符預期,此外,也難保正式機的資料庫狀態跟驗證測試用的資料庫差不多。

再看看另個例子,假設我們現在剛把後端程式寫好,要佈到正式機上。為了讓後端程式正常運行,我們要先手動執行 SQL 指令來初始化資料庫,於是乎,和上面差不多的問題又再度出現,共通點都是要手動執行 SQL 指令。為了解決這樣的情形,migration 就誕生了。

為什麼要寫 Migration

  • 避免手動調整或建置資料庫時出現問題
  • 加快部署速度
  • 降低開發環境和正式環境的資料庫差異

Mingration in Entity Framework

大家最常用的 Entity Framework (EF)其實已經有 migration 的支援了,不過在使用前,需要安裝 ef 的 cli 工具。微軟官方推薦使用 dotnet cli 進行安裝,因此這裡都用 dotnet cli 安裝的版本來示範。

安裝教學

建立初始化的 migration

如果你的專案過去都沒有使用過 migration,下面的指令會掃描當前目錄的專案,並自動建立名為 InitialCreate 的 migration。

1
dotnet ef migrations add InitialCreate

這個指令掃描完專案後,會找出你有使用到的 entity,並自動根據你的 entity 定義產生生成 schema 的程式碼。但在套用之前,應該一律手動檢查,並確認是否與你的需求相符,若是和需求不符,就必須手動修改。

在這裡檢查的意義,除了確認生成工具有沒有生出不符合當初設定的 entity 外,也可以再次確認資料庫和 entity 對應到的設定是否是一樣的。另外還有幾種情形,其一是想在 migration 中增加自訂邏輯,此時只能手動撰寫該部分程式碼,其二是工具在判斷和最近一次 migration 差異時出錯,此時也必須手動修改。幸運的是,絕大多數的狀況 ef 都幫我們處理得好好的。

生成的 migration 預設會被放在 Migrations 資料夾內,請一定要加入版控,migration 本身和程式的版本息息相關,因此一定要納入版控,除此之外,也不要任意刪除已經套用過的 migration,否則會造成 migration 紀錄損壞。

壞了有幾種解決辦法,如果你是不小心誤刪的,那用版控系統拉回來就好,但如果沒做版控,只能把 migration 記錄和 schema 清掉重來了(手動嘗試救援可能可行,後面講到 migration 紀錄原理再講)。不過只要不亂刪 migration 或有人搞破壞亂改 migration 紀錄,migration 是不太可能壞的。

migration 檔名會長類似這樣: 20230202055946_InitialCreate.cs

前面那串數字是時間戳,後面則是 migration 名稱,時間戳會被 ef 用來判斷 migration 的套用的先後順序,請記得不要亂改檔名。

migration 會有數個檔案,但我們只要修改其中一個。

  1. 20230202055946_InitialCreate.cs
  2. 20230202055946_InitialCreate.Designer.cs
  3. AssetsContextModelSnapshot

這些是我碰到的專案例子,所以檔名會略有不同,但大家應該都看得懂

第一個是我們需要觀看並修改的檔案,它的作用是運行程式碼對資料庫進行對應操作,因此你想對資料庫做什麼操作都直接寫在這裡即可,剩下兩個則是 ef 自己維護並使用的檔案,具體功能應該與 cli 有關。

點開第一個檔案可看到 InitialCreate 的 class,且該 class 繼承 Migration 並實作 UpDown 方法。Up 定義該 migration 執行時,資料庫應該要做的所有修改,Down 則是在復原 Up 進行的操作。很顯然的,它希望能做到可復原的修改,但事實上,完全可復原的修改是不可能的。

假設原本使用者名稱長度可為 30 字,經過新的 migration 修改變為 10 字,而後的 migration 又將名稱長度改為 30 字,被截斷的資料依舊不會復原。

正式資料庫在程式上線前應永遠要做備份,以避免任何狀況發生。是否要用 migration 來運行資料庫備份其實有討論的空間,但我個人認為不適合。

備份永遠都要做,相信大家都認同,但為什麼我認為不該在 migration 裡面做呢? 我的想法大致如下:有學過 SOLID 原則的人,肯定讀過其中的單一職責原則,雖然 SOLID 原則應該套用在 class 身上而不是 migration,但其理念依舊適用。此處若將 migration 加入資料庫備份和還原的功能,migration 就要知道備份資料庫的存在,且同時還要知道還原時要用什麼規則去還原。光第一個問題,就強制讓我們的應用程式和備份資料庫耦合了,第二個問題也會造成 migration 邏輯複雜,相信這不是一件好事吧?

套用初始化的 migration

請注意,接下來的內容會影響當前的資料庫 schema,如果你用到的 DB 不是乾淨的,請備份後清空再嘗試,不然一定會遇到衝突(除非你是全新專案)

在建立完 migration 後,可以執行下面的指令套用所有的 migration

1
dotnet ef database update

這個指令會讀取使用的資料庫內的 migration 紀錄,並套用尚未套用的 migration。migration 紀錄會被存放在 __EFMigrationsHistory 的 table 內,table 也只有兩個欄位。第一個是 MigrationId,用來記錄運行過的 migration,其二則是 ProductVersion,用來記錄運行的 ef 版本。

用這種儲存方式,ef 就可以比對出哪些 migration 尚未運行,並且應該執行後加入到 migration 紀錄中。

ProductVerion 應該是為了提供 ef 不同版本之間兼容用的

前面提過的手動救援,可以嘗試手動把被刪掉的 migration 從這張 table 裡面拿掉,不過依舊不保證會動,畢竟你的 database schema 跟 migration 追蹤到的不一樣,依舊要手動調整為 migration 的當前狀態。

模擬資料庫異動

當有新需求發生,我需要在表裡面加個 column 時怎麼辦呢? 我們一樣先改好 entity 的定義,接著依樣畫葫蘆

1
dotnet ef migrations add AddNewNameColumnInAsset

此時 ef 會比對舊的 entity 定義,並且生成對應的修改程式碼。我們只需要確認它生的和我們的需求是否相符即可。

migration 名稱最好有意義,讓人一眼看出 database 的異動

當 migration 確定沒問題後,再根據 migration 調整資料庫

1
dotnet ef database update

上了正式機或 K8S 呢?

其實這種情況都對應到 DB 沒有做任何處理的情形。

因為 migration 本身就很適合處理這種狀況,所以處理起來很簡單,我們只要將應用程式會用到的資料庫建好再運行 migration 即可。

在一般的情形(資料庫是獨立 server),我們手動連上資料庫,並執行 SQL 指令建一個 DB 給應用程式用即可,若是 K8S 或 Docker 這種環境,可以跟負責人討論看看,原則上是在資料庫容器 init 時就可以執行腳本自動把要用到的 DB 建起來,但如果 DB 共用,就照著一般處理方式來吧。

或許你會有疑問,正式機環境也要安裝 ef 的 cli 工具才能跑 migration 嗎?感覺好像哪裡怪怪的。

當然,這是一種處理的方式,但絕對稱不上好。微軟官方提出了數種 solution,其中最推薦的是利用 cli 先生成 migration 會用到的 SQL 語句,再手動丟上去資料庫執行,這樣可以避免在正式機上安裝 .NET SDK 和 cli 工具以及原本程式的 source code。具體做法如下:

1
2
3
4
5
6
# 從全空的 db 跑到最新的 migration
dotnet ef migrations script
# 從 AddNewTables 跑到最新的 migration
dotnet ef migrations script AddNewTables
# 從 AddNewTables 跑到 AddAuditTable
dotnet ef migrations script AddNewTables AddAuditTable

這樣子的做法有個小缺點。上面的 code 生成的 SQL 語句不會檢查當前 migration 的執行狀態(理論來講這些是你應該知道的),因此你要在運行之前先知道自己想把 migration 從哪個狀態跑到哪個狀態。不過微軟還是提供了一個選項,可以只套用當前資料庫缺少的 migration(原理就是加一堆 if)。

1
dotnet ef migrations script --idempotent

參考資料

https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli