EF Core Migration 的價值不只是自動產生資料表異動,而是把模型、資料庫結構與部署紀錄連成可審查的變更鏈。真正可靠的流程不會把 database update 當成第一步;它會先固定工具版本、建置目前程式碼、檢閱 Up、Down 與 snapshot,再在可丟棄資料庫重現,最後才交付經過審核的部署產物。
本文聚焦「EF Core Migration 安全部署」的搜尋意圖,說明如何避免過期組件、誤判欄位改名、歷程表與實際結構漂移,以及未審查 SQL 直接套用等風險。範例只使用本機 SQLite 測試專案,不連線到長期保存的資料庫,也不執行真實業務資料操作。
為什麼 Migration 不能等同於自動部署
Migration 是模型差異的程式化描述,不是資料庫真相的保證;產生成功只代表工具能建立差異檔,仍須人工審查操作順序、資料保留方式與目標環境目前狀態。
EF Core 會依目前模型與 snapshot 推導差異,建立包含 Up、Down 的 migration 類別,並更新 model snapshot。這套機制能追蹤預期結構,卻不知道某個欄位是否曾由外部腳本建立、某次歷程是否遺失,或兩個名稱相近的欄位是否真的代表 rename。官方的 Managing Migrations 也明確要求檢查產生的程式碼,尤其是可能造成資料遺失的操作。
因此應把 migration 視為待審查的變更提案。審查者至少要回答四個問題:差異是否只包含本次需求;刪除與改名是否保留資料;欄位限制收緊前是否先處理既有不相容值;Down 是否真的能回復,或至少明確標示不可逆條件。若無法回答,就不應讓變更進入後續部署階段。
這個定位也能降低團隊並行開發的混亂。兩個分支可能同時從相同 snapshot 產生 migration,合併後的順序與 snapshot 必須重新檢查。單純修改檔名或識別字,若沒有重新建置與列出 migration,可能留下工具無法辨識或永遠顯示 pending 的狀態。
先建置、再產生、再重新建置
穩健順序是先建置目前模型,再產生 migration,檢閱產物後重新建置;使用 --no-build 只能省略已確認不需要的建置,不能讓舊組件成為差異來源。
dotnet ef 的部分命令預設會建置專案;--no-build 則明確略過建置。若輸出目錄仍是上一版組件,工具看到的模型就可能落後於原始碼。更容易忽略的是:新增 migration 之後,剛生成的類別尚未進入先前組件。若立即用 migrations list --no-build,清單可能看不到新 migration。本文的排程實測第一次正好重現此失敗,重新建置後清單才正確顯示 pending 項目。
建議把流程拆成可稽核關卡:
- 鎖定 SDK、
dotnet-ef、EF Core provider 與 Design 套件版本,先執行 restore 與 build。 - 執行
migrations add,逐行查看新 migration 的Up、Down與 snapshot 差異,不接受不明的刪欄、重建表或額外欄位。 - 再次 build,執行
migrations list,確認名稱、順序與 pending 狀態符合預期。 - 只在可丟棄資料庫執行
database update,再查詢實際 schema 與 migration history。 - 產生 SQL script 或 bundle,將可部署產物與審查紀錄一起交付。
版本固定不代表版本永遠安全。本文的隔離測試固定使用 .NET SDK 10.0.400、dotnet-ef 10.0.8、EF Core Design 10.0.8 與 SQLite provider 10.0.8,restore 同時回報其 SQLite 原生相依套件有高嚴重性 advisory。這不影響本次「可丟棄本機資料庫」功能驗證的真實結果,但表示該組合不應未經套件風險檢查就複製到長期專案。導入前應執行 dotnet list package --vulnerable --include-transitive,並依官方修補資訊更新相依版本。
欄位改名、NOT NULL 與資料保留
涉及既有資料時,安全 migration 應先建立可承接資料的新結構、以決定性規則回填並驗證,再收緊限制或移除舊結構,不能盲信自動推導的 rename。
EF Core 無法理解所有業務語意。當模型同時刪除舊屬性並加入新屬性,工具可能產生 DropColumn 加 AddColumn,而不是預期的 rename;它也可能把多個相似欄位配對成錯誤改名。若直接執行,資料可能消失或落入錯誤欄位。正確作法是閱讀每一個 operation,必要時改為「新增、回填、驗證、切換、刪除」的漸進式流程。
把 nullable 欄位改成 NOT NULL 也有順序要求。先修改限制,資料庫會因既有 NULL 而拒絕;先用明確規則回填,再驗證不相容列數為零,才執行 AlterColumn 與索引建立。預設值必須具有業務意義,不能只是為了讓 migration 通過。若無法定義合法預設值,應將資料修正列為獨立、可審查的前置工作。
Down 同樣需要資料觀點。把兩欄合併成一欄後,回復時未必能重建原始分布;刪除欄位後也無法憑空恢復內容。此時不要寫一個表面可執行、實際會破壞資料的假回復。應清楚說明限制,並以資料備份、向前修復 migration 與經演練的復原程序處理。本文不提供長期資料庫操作命令,因為可逆性必須依實際 schema 與資料契約個別設計。
歷程表與實際 schema 必須雙向核對
__EFMigrationsHistory 只記錄哪些 migration 被登記為已套用,不能單獨證明資料表、欄位、索引與資料約束真的符合模型;驗收必須同時檢查歷程與結構。
常見漂移有兩種。第一種是 schema 已更新,但 history 因還原、複製或不完整流程缺少紀錄;第二種是 history 顯示已套用,實際結構卻缺少部分物件。前者若直接執行 pending migration,可能重複建立物件;後者若只看 history,則會把缺陷誤判為完成。兩種狀況都不應靠直接改 history 來掩蓋。
安全處理順序是先停止自動套用,在隔離複本逐一核對 migration 預期物件,再決定修復方式。補登 history 只能是「已驗證結構完整後的記帳修復」,不能用來假造部署成功。若結構不完整,應建立向前修復 migration 或經審查的修正腳本。任何 history 異動都需要備份、同儕審查與明確回復方案;排程環境不執行這類操作。
官方的 Applying Migrations 說明多種套用方式,並指出 SQL script 可供審查、調整、封存與在不同時間執行。相較於應用程式啟動時直接遷移,經審查的 script 或 migration bundle 更適合需要權限分離與變更紀錄的流程。選擇哪一種產物仍要看 provider、交易支援、回復需求與部署治理,而不是只看命令是否方便。
驗證與重現:本機 SQLite sandbox
以下流程驗證三個可觀察事實:新 migration 重新建置後會列為 pending、更新後會出現在 applied 清單,以及產生的 SQL 同時包含目標資料表與歷程表操作。
前置條件為 .NET SDK 10.0.400,並使用全新、可刪除的資料夾。測試版本固定為 dotnet-ef 10.0.8、Microsoft.EntityFrameworkCore.Design 10.0.8 與 Microsoft.EntityFrameworkCore.Sqlite 10.0.8。模型只有 WorkItem(Id, Title),資料庫檔名為 verification.db,不含任何真實資料。Program.cs 以 Database.MigrateAsync() 套用 migration,接著輸出 GetAppliedMigrationsAsync() 結果。
- 執行 restore 與 Release build,再執行
migrations add InitialCreate --no-build;人工確認Migrations目錄包含 migration、designer 與 snapshot。 - 再次 Release build,執行
migrations list --no-build;通過條件是InitialCreate顯示為 pending,若顯示找不到 migration 即失敗。 - 執行
database update --no-build與dotnet run --no-build;通過條件是輸出同一個InitialCreate識別字且程序成功結束。 - 產生從
0到InitialCreate的 SQL;通過條件是檔案同時含WorkItems建表與__EFMigrationsHistory操作。
dotnet new tool-manifest
dotnet tool install dotnet-ef --version 10.0.8
dotnet restore
dotnet build --configuration Release
dotnet tool run dotnet-ef migrations add InitialCreate --no-build --configuration Release
dotnet build --configuration Release
dotnet tool run dotnet-ef migrations list --no-build --configuration Release
dotnet tool run dotnet-ef database update --no-build --configuration Release
dotnet run --no-build --configuration Release
dotnet tool run dotnet-ef migrations script 0 InitialCreate --no-build --configuration Release --output migration.sql
排程環境已實際執行上述非破壞性 sandbox。真實結果為:restore 與兩次 build 完成且 build 為零錯誤;新 migration 在重新建置後顯示 (Pending);database update 套用 InitialCreate;程式列出相同識別字;SQL 檔的兩個標記檢查通過。第一次刻意保留的流程觀察也證明,新增後若未重新建置就搭配 --no-build,migrations list 會回報找不到 migration。restore 的相依套件 advisory 已如實記錄,因此本結果只證明功能行為,不構成安全核准或效能結論。
部署前檢查清單與結論
可靠交付的最低門檻是來源可追溯、差異經人工審查、sandbox 能重現、部署產物可封存,而且歷程表與實際 schema 都有獨立通過條件。
交付前逐項確認:工具與 provider 版本已固定;restore 沒有未處理的套件風險;migration 名稱與順序唯一;Up 沒有需求外操作;Down 的限制已揭露;欄位改名不會變成資料遺失;限制收緊前已驗證資料;SQL script 或 bundle 已由第二人審查;部署身分只具必要權限;套用後會檢查 history、欄位、索引、約束與關鍵資料分布;失敗時有停止條件與向前修復方案。
核心結論很簡單:EF Core Migration 不是「模型改完就執行命令」,而是一條需要建置、差異審查、資料保留設計、隔離重現與雙向驗收的變更鏈。把 --no-build、history 與自動產生 rename 當成方便工具,而不是可信任的真相,才能讓資料庫演進保持可追溯且可判定。