EF CodeFirst 必須要解決的問題
Entity Framework有三種模式:Model First、DB First和 CodeFirst,這裡只談CodeFirst。實際專案中如果採用了CodeFirst,那麼必定會碰見下面這些問題:而且必須解決,否則開發及專案迭代過程中必定會有各類的困惑,以至於放棄使用EF CodeFirst。
以本人對EF CodeFirst 的學習過程,這些問題有:
問題1:資料庫的表和模型(Model)的對映匹配衝突了怎麼辦?比如:先用CodeFirst從無到有建立了資料庫,如果後續的開發過程中直接修改了資料庫,直接修改專案裡的Model匹配資料庫的修改就可以了嗎?或者修改Model後對應直接修改資料庫表就可以了嗎?
要回答這個問題,實際做一做也許就能找到客觀的答案:
這裡使用VS2017 新建一個類庫專案和一個控制檯專案,類庫中新增EntityFramework的引用;
接著在Entity層中新建一個UserEntity模型:
public class UserEntity { public Guid Id { get; set; } public string UserName { get; set; } public string UserDesc { get; set; } public DateTime? CreateDate { get; set; } }
新增一個Entity和資料表的對映關係:如下面的程式碼所示:這裡把User 指向資料表UserT, 並且Id為主鍵,且GUID型別自動由資料庫生成:(EF 建立UserT表建立了DB預設值約束:ALTER TABLE [dbo].[UserT] ADD DEFAULT (newsequentialid()) FOR [Id])
public class UserMap : EntityTypeConfiguration<UserEntity> { public UserMap() { this.ToTable("UserT"); this.HasKey(t => t.Id); this.Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } }
定義一個上下文的引用,派生至System.Data.Entity.DbContext, 用於資料庫讀寫:
public class SqlServerDbContext : DbContext
{
public SqlServerDbContext() : base("name=EFCodeFirst")
{
Database.SetInitializer<SqlServerDbContext>(new CreateDatabaseIfNotExists<SqlServerDbContext>());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
dynamic user = Activator.CreateInstance(typeof(UserMap));
modelBuilder.Configurations.Add(user);
base.OnModelCreating(modelBuilder);
}
}
回到Console專案中:新增App.config
新增資料庫連線字串:
<connectionStrings>
<add name="EFCodeFirst" connectionString="data source=.;initial catalog=TestDB;persist security info=True;user id=sa;password=111111;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>
在main函式執行新增User操作:
static void Main(string[] args)
{
using (SqlServerDbContext context = new SqlServerDbContext())
{
context.Set<UserEntity>().Add(new UserEntity() { UserName = "U001", UserDesc = "add user" });
context.SaveChanges();
}
Console.WriteLine("OL");
Console.Read();
}
執行測試結果:
回到問題:直接修改EF 自動生成的db表,先做如下實驗:
實驗1:新增無相關性的表,例如新建T1表;此時我們的EF測試程式中沒有新增T1表的模型及對映,自然不會有衝突;
實驗2:在UserT表新增列 CreateBy:然後執行程式,新增User沒有問題;
實驗3:改變程式中UserEntity的屬性UserName 成UserDisplayName, 資料庫UserT表的欄位對應修改一致;此時再次測試
看到上圖提示很明顯了,上下文的模型已經在資料庫建立後發生更改了;要快速解決這個問題可以這樣:
public SqlServerDbContext() : base("name=EFCodeFirst")
{
Database.SetInitializer<SqlServerDbContext>(null);
}
原理是:EF中DbContext首次載入時,OnModelCreating會檢查__MigrationHistory表,如果DbContext與模型不匹配則會報錯:
可以用Sql Profiler檢測下sql的執行,搜尋_MigrationHistory,可以看下圖所示的sql執行:
Database.SetInitializer<SqlServerDbContext>(null);設定null後將不在檢測__MigrationHistory,首次執行EF時也不會自動生成資料庫,且還會報錯:基礎提供程式在 Open 上失敗;SqlException: 無法開啟登入所請求的資料庫 。登入失敗。
使用者 'sa' 登入失敗。
這裡建議是當資料庫生成後把__MigrationHistory的檢測設定為null,可以優化EF首次執行的載入效率;
當然,你還可以換其他的Database.SetInitializer策略:
比如:DropCreateDatabaseIfModelChanges 、DropCreateDatabaseAlways等,但如果是正式環境,這樣做會有沒頂之災。
話題再轉回來:如果不設定null,解決模型變更後的異常:方法可以這樣:
簡單來說就是:enable-migration_> add-migration_>update-database
詳細來看下過程:
設定Entity層為啟動專案(重要,否則報異常):VS2017中選單:“工具”——>"Nuget包管理器"——>"程式包管理器控制檯"
此時再來看下資料庫結構和__MigrationHistory表中的變化
這裡有個一個缺陷:UserName改為UserDisplayName後 EF的處理是把UserName欄位刪除,然後新增UserDisplayName欄位:
public partial class user : DbMigration
{
public override void Up()
{
AddColumn("dbo.UserT", "UserLoginAccount", c => c.String());
AddColumn("dbo.UserT", "UserDisplayName", c => c.String());
DropColumn("dbo.UserT", "UserName");
}
public override void Down()
{
AddColumn("dbo.UserT", "UserName", c => c.String());
DropColumn("dbo.UserT", "UserDisplayName");
DropColumn("dbo.UserT", "UserLoginAccount");
}
}
這樣的處理顯然是不可接受的,所以在實際開發專案中應該避免這樣的操作,如果一定需要改列名,可以嘗試保留原來的列,新增一列處理,最大限度保留原有資料;
回答第一個問題:1:資料庫的表和模型(Model)的對映匹配衝突了怎麼辦?比如:先用CodeFirst從無到有建立了資料庫,如果後續的開發過程中直接修改了資料庫,直接修改專案裡的Model匹配資料庫的修改就可以了嗎?或者修改Model後對應直接修改資料庫表就可以了嗎?
答:如果EF Database初始化策略為null:Database.SetInitializer<SqlServerDbContext>(null);
這樣做是可以的。即使不是為null 的策略,如果資料庫的修改同模型的匹配不會發生衝突,例如資料建立唯一約束,建立索引,新增欄位等操作也不會引起EF 報上下文資料建立後的模型改變異常;其他情形則需要通過EF 資料遷移來完成;
問題2:專案上線正式環境後,如果程式調整,正式環境資料庫如何升級;
這裡有兩種方式:方式一:執行update-database -Verbos 則可以生成此次更新的資料庫指令碼,把它記錄下來放在正式環境中執行就可以了;如下圖所示:
實際開發中可能有多次迭代更新,這樣就需要記錄每次update 的升級指令碼,如果每次更新是不同的開發人員來完成,容易造成混亂,不方便管理;
方式二:自動遷移更新資料庫:
internal sealed class Configuration : DbMigrationsConfiguration<MyCodeFirst.Entity.SqlServerDbContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = false;
ContextKey = "MyCodeFirst.Entity.SqlServerDbContext";
}
protected override void Seed(MyCodeFirst.Entity.SqlServerDbContext context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data.
}
}
在SqlServerDbContext的建構函式中使用MigrateDatabaseToLatestVersion 方式:
Database.SetInitializer<SqlServerDbContext>(new MigrateDatabaseToLatestVersion<SqlServerDbContext, MyCodeFirst.Entity.Migrations.Configuration>());
這樣EF框架自動幫我們做好了資料庫的升級;
問題3:實際開發中,資料庫都有專人負責設計,資料庫在前,此時CodeFirst模式如何使用?
CodeFirst 程式碼優先:模型是先行設計的,資料庫通過模型及模型到資料庫表的對映來完成的。比較適合領域驅動設計的開發方式。如果是資料設計已經存在,EF也可以使用CodeFirst模式。
如下圖示:
以這種方式新增的Model實際使用時會提示xxx物件已經存在:
通常的原因是EF會查詢_MigrationHistory, 比對Model 不存在於資料庫中則會生成Create Table 的語句;
處理的方式是不生成Create Table指令碼並能在_MigrationHistory裡自動遷移就可以了。做法是:
如下圖所示:先add-migration xxx
EF自動生成遷移歷史後,開啟最後生成的遷移檔案,註釋掉其中的CreateTable,
然後:update-database
再次執行程式,則不會報物件已經存在的異常了。註釋的CreateTable最好取消註釋,防止切換資料庫連線自動遷移不能生成Create Table指令碼。
個人的困惑:覺得使用EF CodeFirst就要通過手工編寫模型,後生成DB。但手工編碼不直觀,而且工作量大,有點費力。如果是藉助一些工具來輔助設計,通過工具來生成模型,又有些像Model First。即使自己編寫程式來自動生成程式碼也是要先有可以參照的資料來源,比如先有資料庫,從資料庫讀取表的欄位欄位及型別等資訊後程序批量生成模型,這樣做就有些像DB First。且EF發展後面只有CodeFirst。也許是沒有真正領會到CodeFirst的精髓!
總結一下:選擇EF CodeFirst不管通過何種方式來生成模型,需要保證資料庫的表實體和程式模型的對應關係,碰見CodeFirst提示的衝突時,可以通過設定自動遷移或手動遷移完成。知道EF CodeFirst執行原理後可以就開發過程中碰見的各類問題靈活處理。在團隊開發過程中,異常或衝突或許會經常發生,要制定一些規則或做好技術防範:比如專人負責遷移,做好備份或做好原始碼管控,萬一出現錯誤還可以遷移到上一步等。