EF執行資料庫Update指令時,由於在UI上都會挾帶PK ItemId, 因此我們常用Attach(entity)方式來取代得多下一道Select指令的問題:
// 一般作法,得多一道DB Select指令
var entity = dbContext.Entities.Single(x => x.ItemId = 1);
entity.ItemText = "abc";
entity.ItemKey = 0;
dbContext.SaveChanges();
// Attach方式,不會下一道DB Select指令
EntityType entity = new EntityType();
entity.ItemId = 1;
dbContext.Entities.Attach(entity); // EntityState = Unchanged
entity.ItemText = "abc";
entity.ItemKey = 0;
//dbContext.Entry(entity).State = EntityState.Modified; // 此行不需寫,因為它會讓DB修改所有欄位值,而非指定的欄位,這也是一個隱藏Bug地雷
dbContext.Configuration.ValidateOnSaveEnabled = false; // 因為Entity有些欄位必填,若不避開會有Validate錯誤
//dbContext.ChangeTracker.DetectChanges(); // 不需撰寫此行,因為dbContext.Configuration.AutoDetectChangesEnabled = true (Default)會自動呼叫
dbContext.SaveChanges();
首先令人覺得好奇的是,只是修改一個很單純POCO Entity的屬性值,為何EF會知道異動了? 原來當你Attach(entity)後,EF透過DbSet宣告時virtual關鍵字,暗地把原本的POCO類別ref位址指向內部DynamicProxy類別,這Proxy為每一個屬性Property作了延伸處理,讓內部ChangingTracker記錄了每個屬性的Old, New值及IsModified狀態。可查看一下EF PropertyEntry源碼宣告,就知大概的追蹤異動處理手法。
因此,當一個Property改值時,它會比對新舊值,一旦不一樣就會把EntityState狀態由Unchanged變為Modified,並在SaveChanges()之前自動呼叫DetectChanges()來搜集每個Properties的新舊值變動狀況。在這樣的原則下,上述用Attach(entity)省下DB Select指令的寫法,就藏著一個可怕的Bug,原來只要Property的值沒有異動到,DB指令裏該欄位就不會修改到,這點是筆者尚未明白EF偵測異動原理前遇到的血淚地雷經驗。
// 調整如下,才能確保要修改的欄位值會被DB更新
EntityType entity = new EntityType();
entity.ItemId = 1;
entity.ItemKey = -1; // Entity int類型屬性,預設是0,所以要指定不可能的值造成異動
dbContext.Entities.Attach(entity); // EntityState = Unchanged
entity.ItemText = "abc"; // 字串通常不需要在Attach()前先調值,因為string預設是null,而UI取值通常是String.Empty
entity.ItemKey = 0; // 若事先沒調值,當有UI把某值改為0時,DB執行時並不會修改到此欄位,造成Bug
dbContext.Entry(entity).Property(x => x.ItemKey).IsModified = true; // 可以直接使用這方式強制某欄位要更新,只是查詢集合耗效能而己
dbContext.Configuration.ValidateOnSaveEnabled = false; // 因為Entity有些欄位必填,若不避開會有Validate錯誤
dbContext.SaveChanges();
由於EF預設下都會自動偵測異動(dbContext.Configuration.AutoDetectChangesEnabled=true),尤其在以下操作時都會自動呼叫DetectChanges()比對所有的entry集合的每一個屬性Properties的新舊值,若在大量操作時難免效能不佳,因此特殊情況時,會讓dbContext.Configuration.AutoDetectChangesEnabled=fase,完成複雜的操作,才只作一道dbContext.ChangeTracker.DetectChanges()比對異動。
- DbSet.Find
- DbSet.Local
- DbSet.Remove
- DbSet.Add
- DbSet.Attach
- DbContext.SaveChanges
- DbContext.GetValidationErrors
- DbContext.Entry
- DbChangeTracker.Entries
EntityState是EF執行DB指令重要的判斷依據,不了解底層原理就直接使用Attach(entity),其實隱藏了很多可能Bug錯誤。不過,或許大多數EF使用者不在乎修改資料時多下一道DB Select指令,也就不會遇到這樣的問題了。EF內部行為已為大多數DB操作行為作了合理推斷,不過每個開發者追求效能的心態層次不一樣,想自由穿梭其間,就得多閱讀理論基礎就是了。