使用EF Core的Code First,在設計階段,直接使用Database.EnsureCreated()
和EnsureDeleted()
可以快速刪除、更新最新的資料結構。由於沒有什麼資料,刪除的風險非常低。但是對於已經投入生產的資料庫,這個方法就絕對不可行了。
考慮以下場景:
專案已經上線,一直使用本地測試資料庫進行開發,本地已經增加和修改了較多資料庫表結構,線上資料龐大且實時更新,現在測試完畢需要進行上線。
如果需要更新生產資料庫,我能想的有兩種方法:
Migration
從資料庫開始設計的時候,就使用EF Migration,保證資料庫能夠與程式碼同步,不過操作的時候,需要極為小心,務必要檢查生成的更新資料庫程式碼,直接連線生產資料庫,
需要注意的事項:
Migration
,任何時候都不要使用Context.Database.EnsureCreated或者EnsureDeleted語句。Add-migration
之後,不要刪除生成的Migration檔案,這些檔案記錄了資料結構的變化歷史。如果一開始就沒有使用migration進行同步的話,那麼使用EF Core將無法直接更新,我們需要變通一下:
首先需要資料庫的資料結構逆向到模型,我們使用Scaffold
就可以了,詳細檔案就可以檢視這裡,需要注意的是,我們的場景下,已經有修改好的DataContext與Model,在進行scaffold的過程中,一定要指定outputdir和context,不要和當前的檔案衝突。
根據自己的喜好,選擇是否採用-DataAnnotations,另外也可以使用-table指定需要修改的表,沒有被指定的表,將保持原樣。預設EF Core會按照自己的命名規則重新命名,如果你想保留自己的套路,那麼使用-UseDatabaseNames引數。
輸出的模型我指定放在Models資料夾,原來的Models資料夾,我改成了Models1,並且更換了名稱空間以保證專案現在能夠正常編譯。
Add-Migration
。add-migration initialcreate -context exportedContext
這樣會在Migrations資料夾下面生成一個snapshot和一個migration檔案。snapshot是當前資料庫的跟蹤,另外一個是運用update-database時系統會執行的操作。裡面有一個Up()
和一個Down()
方法,Up是執行更新時EF對資料庫的操作,Down是回滾當前更改。由於這是第一次執行add-migration,EF Core會認為資料庫現在還是空的,因此兩個方法都有大量的語句,我們刪除所有create和drop相關的語句,我這邊是全部刪除了,只留下空方法。
前面準備工作已經到位了,這一步將直接運算元據庫了。使用update-database
將當前的migration更新到資料庫,由於我們現在的資料結構和生產資料庫的資料結構一模一樣,實際上我們不需要執行什麼操作(刪除了Up、Down內部的程式碼),執行Update-Database只是讓EF Core將Models和生產資料庫建立聯絡。
我理解只是新增
__EFMigrationsHistory
中的記錄,以便EF Core後續追蹤。
將Models1中的檔案覆蓋Models中的檔案,由於型別命名的差異,可能會提示一些錯誤,按照自己的習慣修改就好了。接下來是循序漸進,一點點修改模型,並經常add-migration,觀察生成的語句是否正常。
由於我使用了Identity
,在資料中有對應的AspNet
開頭的表,這些表我並不在本系統中使用(其他系統需要用),因此我刪除了對應的模型、snapshot、DbContext記錄,執行Add-Migration,生成了如下檔案:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaim");
migrationBuilder.DropTable(
name: "AspNetUserClaim");
migrationBuilder.DropTable(
name: "AspNetUserLogin");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserToken");
migrationBuilder.DropTable(
name: "AspNetRole");
migrationBuilder.DropTable(
name: "AspNetUser");
}
說明現在已經能夠正常跟蹤我們的修改了,不過我這裡需要保留對應的表,因此刪除up與down的所有內容。
注意以下幾點:
如果使用fluentAPI
,那麼模型對應的表名稱會直接在fluentAPI
中直接指定,只修改模型的名稱沒有任何效果。修改的話,可以修改對應的fluentAPI,或者換用Annotation
對於修改主鍵、索引等內容的情況,如果不是通過EF Core建立的資料庫,那麼命名規則可能不一樣。對於postgresql資料庫,可以用這個查詢名稱,然後修改對應的migration檔案內容即可。
SELECT * FROM pg_CONSTRAINT
對於使用兩列或者以上列作為複合主鍵的情況,使用EnsureCreated
方法是可以識別Annotation
形式的主鍵的。
[Key]
[Column(Order = 1)]
public string DeviceId { get; set; }
[Key]
[Column(Order = 2)]
public long Timestamp { get; set; }
使用Migration的時候,這種形式無法識別,需要在OnModelCreating()
中,使用fluentAPI
:
modelBuilder.Entity<DeviceData>().HasKey(w => new { w.DeviceId, w.Timestamp });
預設Command
執行的超時設定只有30s,對一些大一點的表來說,是不太夠的。可以設定:
optionsBuilder.UseNpgsql("Server=xxxxxxxxxxxxx", opt=>opt.CommandTimeout(3000));
增加命令執行的超時時間。
如果程式使用了appsettings.Development.json
之類的檔案儲存連線字串,那麼需要指定環境是Production
(生產資料庫),否則可能還原到本地資料庫去了。
對於nuget包管理控制檯(使用update-database),執行:
$Env:ASPNETCORE_ENVIRONMENT = "Development"
Update-Database
對於使用dotnet ef工具集的,直接執行:
dotnet ef database update --environment Development
設計資料庫表如果修改列的資料型別(比如從varchar到integer),Postgresql會提示這個問題,導致無法修改。可以在migrationbuilder
中使用sql,按照提示新增"USING "x"::integer"解決。但是這種方法還是不太優雅,手動處理Up()
之後,還需要處理Down()
,否則將無法正確還原。
可以使用分步的方法進行,假設我們需要將Id從varchar
改成int4
。
每次修改儘量少一點,然後update-database
,這樣更容易發現問題,對於有
這種提示的,一定要檢查生成語句中Drop相關的語句。
本地資料庫與生產資料庫,都有__EFMigrationsHistory記錄相關的遷移情況。在生產與本地資料庫中進行切換時,不用擔心順序問題,Update-Database會一個個應用遷移直到最新。
使用Migration能夠降低資料庫同步中很多工作量,合理利用,可以對生產用的資料庫進行熱更新。
注:本文在.NET 6,EF Core 6下測試通過。