EF Code First:數據更新最佳實踐

EF Code First:數據更新最佳實踐

最近在整理EntityFramework數據更新的代碼,很有體會,以爲有分享的價值,因而記錄下來,讓須要的人少走些彎路也是好的。爲方便起見,先建立一個控制檯工程,使用using(var db = new DataContext)的形式來一步一步講解EF數據更新的可能會遇到的問題及對應的解決方案。在得到最佳方案以後,再整合到本系列的代碼中。html

 

1、前言sql

最近在整理EntityFramework數據更新的代碼,很有體會,以爲有分享的價值,因而記錄下來,讓須要的人少走些彎路也是好的。數據庫

爲方便起見,先建立一個控制檯工程,使用using(var db = new DataContext)的形式來一步一步講解EF數據更新的可能會遇到的問題及對應的解決方案。在得到最佳方案以後,再整合到本系列的代碼中。架構

本示例中,用到的數據模型以下圖所示:mvc

  1. 部門:一個部門可有多個角色【1-N】
  2. 角色:一個角色必有一個部門【N-1】,一個角色可有多我的員【N-N】
  3. 人員:一我的員可有多個角色【N-N】

而且,咱們經過數據遷移策略初始化了一些數據:app

初始化數據ide

  1. protected override void Seed(GmfEFUpdateDemo.Models.DataContext context)  
  2. {  
  3.     //部門  
  4.     var departments = new []  
  5.     {  
  6.         new Department {Name = "技術部"},  
  7.         new Department {Name = "財務部"}  
  8.     };  
  9.     context.Departments.AddOrUpdate(m => new {m.Name}, departments);  
  10.     context.SaveChanges();  
  11.  
  12.     //角色  
  13.     var roles = new[]  
  14.     {  
  15.         new Role{Name = "技術部經理", Department = context.Departments.Single(m=>m.Name =="技術部")},  
  16.         new Role{Name = "技術總監", Department = context.Departments.Single(m=>m.Name =="技術部")},  
  17.         new Role{Name = "技術人員", Department = context.Departments.Single(m=>m.Name =="技術部")},  
  18.         new Role{Name = "財務部經理", Department = context.Departments.Single(m=>m.Name =="財務部")},  
  19.         new Role{Name = "會計", Department = context.Departments.Single(m=>m.Name =="財務部")}  
  20.     };  
  21.     context.Roles.AddOrUpdate(m=>new{m.Name}, roles);  
  22.     context.SaveChanges();  
  23.  
  24.     //人員  
  25.     var members = new[]  
  26.     {  
  27.         new Member  
  28.         {  
  29.             UserName = "郭明鋒",  
  30.             Password = "123456",  
  31.             Roles = new HashSet<Role>  
  32.             {  
  33.                 context.Roles.Single(m => m.Name == "技術人員")  
  34.             }  
  35.         }  
  36.     };  
  37.     context.Members.AddOrUpdate(m => new {m.UserName}, members);  
  38.     context.SaveChanges();  

2、總體更新(不考慮更新屬性)網站

情景一:同一上下文中數據取出來更新後直接保存:this

代碼:編碼

  1. private static void Method01()  
  2. {  
  3.     using (var db = new DataContext())  
  4.     {  
  5.         const string userName = "郭明鋒";  
  6.         Member oldMember = db.Members.Single(m => m.UserName == userName);  
  7.         Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  8.  
  9.         oldMember.AddDate = oldMember.AddDate.AddMinutes(10);  
  10.         int count = db.SaveChanges();  
  11.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  12.  
  13.         Member newMember = db.Members.Single(m => m.UserName == userName);  
  14.         Console.WriteLine("更新後:{0}。", newMember.AddDate);  
  15.     }  

代碼解析:操做必然成功,執行的sql語句以下:

  1. exec sp_executesql N'update [dbo].[Members]  
  2. set [AddDate] = @0  
  3. where ([Id] = @1)  
  4. ',N'@0 datetime2(7),@1 int',@0='2013-08-31 13:17:33.1570000',@1=1 

注意,這裏並無對更新實體的屬性進行篩選,但EF仍是聰明的生成了只更新AddDate屬性的sql語句。

情景二:從上下文1中取出數據並修改,再在上下文2中進行保存:

代碼:

  1. private static void Method02()  
  2. {  
  3.     const string userName = "郭明鋒";  
  4.  
  5.     Member updateMember;  
  6.     using (var db1 = new DataContext())  
  7.     {  
  8.         updateMember = db1.Members.Single(m => m.UserName == userName);  
  9.     }  
  10.     updateMember.AddDate = DateTime.Now;  
  11.  
  12.     using (var db2 = new DataContext())  
  13.     {  
  14.         db2.Members.Attach(updateMember);  
  15.         DbEntityEntry<Member> entry = db2.Entry(updateMember);  
  16.         Console.WriteLine("Attach成功後的狀態:{0}", entry.State); //附加成功以後,狀態爲EntityState.Unchanged  
  17.         entry.State = EntityState.Modified;  
  18.         int count = db2.SaveChanges();  
  19.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  20.  
  21.         Member newMember = db2.Members.Single(m => m.UserName == userName);  
  22.         Console.WriteLine("更新後:{0}。", newMember.AddDate);  
  23.     }  

代碼解析:對於db2而言,updateMemner是一個全新的外來的它不認識的對象,因此須要使用Attach方法把這個外來對象附加到它的上下文中,Attach以後,實體的對象爲 EntityState.Unchanged,若是不改變狀態,在SaveChanged的時候將什麼也不作。所以還須要把狀態更改成EntityState.Modified,而由Unchanged -> Modified的改變,是咱們強制的,而不是由EF狀態跟蹤獲得的結果,於是EF沒法分辨出哪一個屬性變動了,於是將不分青紅皁白地將全部屬性都刷一遍,執行以下sql語句:

  1. exec sp_executesql N'update [dbo].[Members]  
  2. set [UserName] = @0, [Password] = @1, [AddDate] = @2, [IsDeleted] = @3  
  3. where ([Id] = @4)  
  4. ',N'@0 nvarchar(50),@1 nvarchar(50),@2 datetime2(7),@3 bit,@4 int',@0=N'郭明鋒',@1=N'123456',@2='2013-08-31 13:28:01.9400328',@3=0,@4=1 

情景三:在情景二的基礎上,上下文2中已存在與外來實體主鍵相同的數據了

代碼:

  1. private static void Method03()  
  2. {  
  3.     const string userName = "郭明鋒";  
  4.  
  5.     Member updateMember;  
  6.     using (var db1 = new DataContext())  
  7.     {  
  8.         updateMember = db1.Members.Single(m => m.UserName == userName);  
  9.     }  
  10.     updateMember.AddDate = DateTime.Now;  
  11.  
  12.     using (var db2 = new DataContext())  
  13.     {  
  14.         //先查詢一次,讓上下文中存在相同主鍵的對象  
  15.         Member oldMember = db2.Members.Single(m => m.UserName == userName);  
  16.         Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  17.  
  18.         db2.Members.Attach(updateMember);  
  19.         DbEntityEntry<Member> entry = db2.Entry(updateMember);  
  20.         Console.WriteLine("Attach成功後的狀態:{0}", entry.State); //附加成功以後,狀態爲EntityState.Unchanged  
  21.         entry.State = EntityState.Modified;  
  22.         int count = db2.SaveChanges();  
  23.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  24.  
  25.         Member newMember = db2.Members.Single(m => m.UserName == userName);  
  26.         Console.WriteLine("更新後:{0}。", newMember.AddDate);  
  27.     }  

代碼解析:此代碼與情景二相比,就是多了14~16三行代碼,目的是製造一個要更新的數據在上下文2中正在使用的場景,這時會發生什麼狀況呢?

當代碼執行到18行的Attach的時候,將引起一個EF數據更新時很是常見的異常:

  1. 捕捉到 System.InvalidOperationException  
  2.   HResult=-2146233079  
  3.   Message=ObjectStateManager 中已存在具備同一鍵的對象。ObjectStateManager 沒法跟蹤具備相同鍵的多個對象。  
  4.   Source=System.Data.Entity  
  5.   StackTrace:  
  6.        在 System.Data.Objects.ObjectContext.VerifyRootForAdd(Boolean doAttach, String entitySetName, IEntityWrapper wrappedEntity, EntityEntry existingEntry, EntitySet& entitySet, Boolean& isNoOperation)  
  7.        在 System.Data.Objects.ObjectContext.AttachTo(String entitySetName, Object entity)  
  8.        在 System.Data.Entity.Internal.Linq.InternalSet`1.<>c__DisplayClass2.<Attach>b__1()  
  9.        在 System.Data.Entity.Internal.Linq.InternalSet`1.ActOnSet(Action action, EntityState newState, Object entity, String methodName)  
  10.        在 System.Data.Entity.Internal.Linq.InternalSet`1.Attach(Object entity)  
  11.        在 System.Data.Entity.DbSet`1.Attach(TEntity entity)  
  12.        在 GmfEFUpdateDemo.Program.Method03() 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 148  
  13.        在 GmfEFUpdateDemo.Program.Main(String[] args) 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 54  
  14.   InnerException: 

緣由正是上下文2中已經有了一個相同主鍵的對象,不能再附加了。

這應該是一個很是常見的場景,也就是必須想辦法解決的場景。其實只要得到現有實體數據的跟蹤,再把新數據賦到現有實體上,就能夠解決問題了,此方法惟一的缺點就是要獲取到舊的實體數據。代碼以下:

  1. private static void Method04()  
  2. {  
  3.     const string userName = "郭明鋒";  
  4.  
  5.     Member updateMember;  
  6.     using (var db1 = new DataContext())  
  7.     {  
  8.         updateMember = db1.Members.Single(m => m.UserName == userName);  
  9.     }  
  10.     updateMember.AddDate = DateTime.Now;  
  11.  
  12.     using (var db2 = new DataContext())  
  13.     {  
  14.         //先查詢一次,讓上下文中存在相同主鍵的對象  
  15.         Member oldMember = db2.Members.Single(m => m.UserName == userName);  
  16.         Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  17.  
  18.         DbEntityEntry<Member> entry = db2.Entry(oldMember);  
  19.         entry.CurrentValues.SetValues(updateMember);  
  20.         int count = db2.SaveChanges();  
  21.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  22.  
  23.         Member newMember = db2.Members.Single(m => m.UserName == userName);  
  24.         Console.WriteLine("更新後:{0}。", newMember.AddDate);  
  25.     }  

代碼中的18~19行是核心代碼,先從上下文中的舊實體獲取跟蹤,第19行的SetValues方法就是把新值設置到舊實體上(這一條很強大,支持任何類型,好比ViewObject,DTO與POCO能夠直接映射傳值)。因爲值的更新是直接在上下文中的現有實體上進行的,EF會本身跟蹤值的變化,所以這裏並不須要咱們來強制設置狀態爲Modified,執行的sql語句也足夠簡單:

  1. exec sp_executesql N'update [dbo].[Members]  
  2. set [AddDate] = @0  
  3. where ([Id] = @1)  
  4. ',N'@0 datetime2(7),@1 int',@0='2013-08-31 14:03:27.1425875',@1=1 

 

總體更新的最佳實現

綜合上面的幾種情景,咱們能夠獲得EF對實體總體更新的最佳方案,這裏寫成DbContext的擴展方法,代碼以下:

  1. public static void Update<TEntity>(this DbContext dbContext, params TEntity[] entities) where TEntity : EntityBase  
  2. {  
  3.     if (dbContext == null) throw new ArgumentNullException("dbContext");  
  4.     if (entities == null) throw new ArgumentNullException("entities");  
  5.  
  6.     foreach (TEntity entity in entities)  
  7.     {  
  8.         DbSet<TEntity> dbSet = dbContext.Set<TEntity>();  
  9.         try 
  10.         {  
  11.             DbEntityEntry<TEntity> entry = dbContext.Entry(entity);  
  12.             if (entry.State == EntityState.Detached)  
  13.             {  
  14.                 dbSet.Attach(entity);  
  15.                 entry.State = EntityState.Modified;  
  16.             }  
  17.         }  
  18.         catch (InvalidOperationException)  
  19.         {  
  20.             TEntity oldEntity = dbSet.Find(entity.Id);  
  21.             dbContext.Entry(oldEntity).CurrentValues.SetValues(entity);  
  22.         }   
  23.     }  

調用代碼以下:

  1. db.Update<Member>(member);  
  2. int count = db.SaveChanges(); 

針對不一樣的情景,將執行不一樣的行爲:

  • 情景一:上面代碼第11行執行後entry.State將爲EntityState.Modified,會直接退出此Update方法直接進入SaveChanges的執行。此情景執行的sql語句爲只更新變動的實體屬性。
  • 情景二:將正確執行 try 代碼塊。此情景執行的sql語句爲更新所有實體屬性。
  • 情景三:在代碼執行到第12行的Attach方法時將拋出 InvalidOperationException 異常,接着執行 catch 代碼塊。此情景執行的sql語句爲只更新變動的實體屬性。 

3、按需更新(更新指定實體屬性)

需求分析

前面已經有總體更新了,不少時候也都能作到只更新變化的實體屬性,爲何還要來個「按需更新」的需求呢?主要基於如下幾點理由:

  • 總體更新中獲取數據的變動是要把新值與原始值的屬性一一對比的,於是總體更新要從數據庫中獲取完整的實體數據,以保證被更新的只有咱們想要改變的實體屬性,這樣進行總體更新時至少要從數據庫中查詢一次數據
  • 執行的更新語句有多是更新全部實體屬性的(如上的情景三),若是實體屬性不少,就容易形成計算資源的浪費(由於咱們只須要更新其中的某幾個屬性值)。
  • 不能只更新指定的實體屬性,有了按需更新,咱們能夠很是方便的只更新指定的屬性,沒有指定的屬性即便值變化了也不更新

需求實現

按需更新,也就是知道要更新的實體屬性,好比用戶要修改密碼,就只是要把Password這個屬性的值變動爲指定的新值,其餘的最好是儘可能不驚動。固然,至少仍是要知道要更新數據的主鍵的,不然,更新對象就不明瞭。下面就以設置密碼爲例來講明問題。

要設置密碼,我構造了一個空的Member類來裝載新密碼:

  1. Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second}; 

而後,咱們想固然的寫出了以下實現代碼:

  1. private static void Method06()  
  2. {  
  3.     Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second};  
  4.     using (var db = new DataContext())  
  5.     {  
  6.         DbEntityEntry<Member> entry = db.Entry(member);  
  7.         entry.State = EntityState.Unchanged;  
  8.         entry.Property("Password").IsModified = true;  
  9.         int count = db.SaveChanges();  
  10.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  11.  
  12.         Member newMember = db.Members.Single(m => m.Id == 1);  
  13.         Console.WriteLine("更新後:{0}。", newMember.Password);  
  14.     }  

而後,在執行第9行SaveChanges的時候引起了以下異常:

  1. 捕捉到 System.Data.Entity.Validation.DbEntityValidationException  
  2.   HResult=-2146232032  
  3.   Message=對一個或多個實體的驗證失敗。有關詳細信息,請參見「EntityValidationErrors」屬性。  
  4.   Source=EntityFramework 
  5.   StackTrace:  
  6.        在 System.Data.Entity.Internal.InternalContext.SaveChanges()  
  7.        在 System.Data.Entity.Internal.LazyInternalContext.SaveChanges()  
  8.        在 System.Data.Entity.DbContext.SaveChanges()  
  9.        在 GmfEFUpdateDemo.Program.Method06() 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 224  
  10.        在 GmfEFUpdateDemo.Program.Main(String[] args) 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 63  
  11.   InnerException: 

爲何出現此異常?由於前面咱們建立的Member對象只包含一個Id,一個Password屬性,其餘的屬性並無賦值,也不考慮是否規範,這樣就定義出了一個不符合實體類驗證定義的對象了(Member類要求UserName屬性是不可爲空的)。幸虧,DbContext.Configuration中給咱們定義了是否在保存時驗證明體有效性(ValidateOnSaveEnabled)這個開關,咱們只要在執行按需更新的保存時把驗證閉,在保存成功後再開啓便可,更改代碼以下:

  1. private static void Method06()  
  2. {  
  3.     Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second};  
  4.     using (var db = new DataContext())  
  5.     {  
  6.         DbEntityEntry<Member> entry = db.Entry(member);  
  7.         entry.State = EntityState.Unchanged;  
  8.         entry.Property("Password").IsModified = true;  
  9.         db.Configuration.ValidateOnSaveEnabled = false;  
  10.         int count = db.SaveChanges();  
  11.         db.Configuration.ValidateOnSaveEnabled = true;  
  12.         Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  13.  
  14.         Member newMember = db.Members.Single(m => m.Id == 1);  
  15.         Console.WriteLine("更新後:{0}。", newMember.Password);  
  16.     }  

與總體更新同樣,理所固然的會出現當前上下文已經存在了相同主鍵的實體數據的狀況,固然,根據以前的經驗,也很容易的進行處理了:

  1. private static void Method07()  
  2.     {  
  3.         Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };  
  4.         using (var db = new DataContext())  
  5.         {  
  6.             //先查詢一次,讓上下文中存在相同主鍵的對象  
  7.             Member oldMember = db.Members.Single(m => m.Id == 1);  
  8.             Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  9.  
  10.             try 
  11.             {  
  12.                 DbEntityEntry<Member> entry = db.Entry(member);  
  13.                 entry.State = EntityState.Unchanged;  
  14.                 entry.Property("Password").IsModified = true;  
  15.             }  
  16.             catch (InvalidOperationException)  
  17.             {  
  18.                 DbEntityEntry<Member> entry = db.Entry(oldMember);  
  19.                 entry.CurrentValues.SetValues(member);  
  20.                 entry.State = EntityState.Unchanged;  
  21.                 entry.Property("Password").IsModified = true;  
  22.             }  
  23.             db.Configuration.ValidateOnSaveEnabled = false;  
  24.             int count = db.SaveChanges();  
  25.             db.Configuration.ValidateOnSaveEnabled = true;  
  26.             Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  27.  
  28.             Member newMember = db.Members.Single(m => m.Id == 1);  
  29.             Console.WriteLine("更新後:{0}。", newMember.Password);  
  30.         }  
  31.     } 

可是,上面的代碼卻沒法正常工做,通過調試發現,當執行到第20行的時候,entry中跟蹤的數據又變回oldMember了,通過一番EntityFramework源碼搜索,終於找到了問題的出處(System.Data.Entity.Internal.InternalEntityEntry類中):

  1. public EntityState State  
  2.     {  
  3.       get 
  4.       {  
  5.         if (!this.IsDetached)  
  6.           return this._stateEntry.State;  
  7.         else 
  8.           return EntityState.Detached;  
  9.       }  
  10.       set 
  11.       {  
  12.         if (!this.IsDetached)  
  13.         {  
  14.           if (this._stateEntry.State == EntityState.Modified && value == EntityState.Unchanged)  
  15.             this.CurrentValues.SetValues(this.OriginalValues);  
  16.           this._stateEntry.ChangeState(value);  
  17.         }  
  18.         else 
  19.         {  
  20.           switch (value)  
  21.           {  
  22.             case EntityState.Unchanged:  
  23.               this._internalContext.Set(this._entityType).InternalSet.Attach(this._entity);  
  24.               break;  
  25.             case EntityState.Added:  
  26.               this._internalContext.Set(this._entityType).InternalSet.Add(this._entity);  
  27.               break;  
  28.             case EntityState.Deleted:  
  29.             case EntityState.Modified:  
  30.               this._internalContext.Set(this._entityType).InternalSet.Attach(this._entity);  
  31.               this._stateEntry = this._internalContext.GetStateEntry(this._entity);  
  32.               this._stateEntry.ChangeState(value);  
  33.               break;  
  34.           }  
  35.         }  
  36.       }  
  37.     } 

第1四、15行,當狀態由Modified更改成Unchanged的時候,又把數據從新設置爲舊的數據OriginalValues了。真吭!

好吧,看來在DbContext中折騰已經沒戲了,只要去它老祖宗ObjectContext中找找出路,更改實現以下:

  1. private static void Method08()  
  2.     {  
  3.         Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };  
  4.         using (var db = new DataContext())  
  5.         {  
  6.             //先查詢一次,讓上下文中存在相同主鍵的對象  
  7.             Member oldMember = db.Members.Single(m => m.Id == 1);  
  8.             Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  9.  
  10.             try 
  11.             {  
  12.                 DbEntityEntry<Member> entry = db.Entry(member);  
  13.                 entry.State = EntityState.Unchanged;  
  14.                 entry.Property("Password").IsModified = true;  
  15.             }  
  16.             catch (InvalidOperationException)  
  17.             {  
  18.                 ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;  
  19.                 ObjectStateEntry objectEntry = objectContext.ObjectStateManager.GetObjectStateEntry(oldMember);  
  20.                 objectEntry.ApplyCurrentValues(member);  
  21.                 objectEntry.ChangeState(EntityState.Unchanged);  
  22.                 objectEntry.SetModifiedProperty("Password");  
  23.             }  
  24.             db.Configuration.ValidateOnSaveEnabled = false;  
  25.             int count = db.SaveChanges();  
  26.             db.Configuration.ValidateOnSaveEnabled = true;  
  27.             Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  28.  
  29.             Member newMember = db.Members.Single(m => m.Id == 1);  
  30.             Console.WriteLine("更新後:{0}。", newMember.Password);  
  31.         }  
  32.     } 

catch代碼塊使用了EF4.0時代使用的ObjectContext來實現,很好的達到了咱們的目的,執行的sql語句以下:

  1. exec sp_executesql N'update [dbo].[Members]  
  2. set [Password] = @0  
  3. where ([Id] = @1)  
  4. ',N'@0 nvarchar(50),@1 int',@0=N'NewPassword2',@1=1 

 

封裝重構的分析

以上的實現中,屬性名都是以硬編碼的形式直接寫到實現類中,做爲底層的封閉,這是確定不行的,至少也要做爲參數傳遞到一個通用的更新方法中。參照總體更新的擴展方法定義,咱們很容易的就能定義出以下簽名的擴展方法:

  1. public static void Update<TEntity>(this DbContext dbContext, string[] propertyNames, params TEntity[] entities) where TEntity : EntityBase  
  2. 方法調用方式:  
  3. dbContext.Update<Member>(new[] {"Password"}, member); 

調用中屬性名依然要使用字符串的方式,寫起來麻煩,還容易出錯。看來,強類型纔是最好的選擇。

寫到這,忽然想起了作數據遷移的時候使用到的System.Data.Entity.Migrations.IDbSetExtensions 類中的擴展方法

  1. public static void AddOrUpdate<TEntity>(this IDbSet<TEntity> set, Expression<Func<TEntity, object>> identifierExpression, params TEntity[] entities) where TEntity : class 

其中的參數Expression<Func<TEntity, object>> identifierExpression就是用於傳送實體屬性名的,因而,咱們參照着,能夠定義出以下簽名的更新方法:

  1. public static void Update<TEntity>(this DbContext dbContext, Expression<Func<TEntity, object>> propertyExpression, params TEntity[] entities) where TEntity : EntityBase  
  2.  
  3. 方法調用方式:  
  4. db.Update<Member>(m => new { m.Password }, member); 

到這裏,如何從Expression<Func<TEntity, object>>得到屬性名成爲了完成封閉的關鍵。仍是通過調試,有了以下發現:

運行時的Expression表達式中,Body屬性中有個類型爲ReadOnlyCollection<MemberInfo> 的 Members集合屬性,咱們須要的屬性正以MemberInfo的形式存在其中,所以,咱們藉助一下 dynamic 類型,將Members屬性解析出來,便可輕鬆獲得咱們想的數據。

  1. ReadOnlyCollection<MemberInfo> memberInfos = ((dynamic)propertyExpression.Body).Members; 

按需更新的最佳實現

通過上面的分析,難點已逐個擊破,很輕鬆的就獲得了以下擴展方法的實現:

  1. public static void Update<TEntity>(this DbContext dbContext, Expression<Func<TEntity, object>> propertyExpression, params TEntity[] entities)  
  2.         where TEntity : EntityBase  
  3.     {  
  4.         if (propertyExpression == null) throw new ArgumentNullException("propertyExpression");  
  5.         if (entities == null) throw new ArgumentNullException("entities");  
  6.         ReadOnlyCollection<MemberInfo> memberInfos = ((dynamic)propertyExpression.Body).Members;  
  7.         foreach (TEntity entity in entities)  
  8.         {  
  9.             try 
  10.             {  
  11.                 DbEntityEntry<TEntity> entry = dbContext.Entry(entity);  
  12.                 entry.State = EntityState.Unchanged;  
  13.                 foreach (var memberInfo in memberInfos)  
  14.                 {  
  15.                     entry.Property(memberInfo.Name).IsModified = true;  
  16.                 }  
  17.             }  
  18.             catch (InvalidOperationException)  
  19.             {  
  20.                 TEntity originalEntity = dbContext.Set<TEntity>().Local.Single(m => m.Id == entity.Id);  
  21.                 ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;  
  22.                 ObjectStateEntry objectEntry = objectContext.ObjectStateManager.GetObjectStateEntry(originalEntity);  
  23.                 objectEntry.ApplyCurrentValues(entity);  
  24.                 objectEntry.ChangeState(EntityState.Unchanged);  
  25.                 foreach (var memberInfo in memberInfos)  
  26.                 {  
  27.                     objectEntry.SetModifiedProperty(memberInfo.Name);  
  28.                 }  
  29.             }  
  30.         }  
  31.     } 

注意,這裏的第20行雖然進行了原始數據的查詢,可是從DbSet<T>.Local中進行的查詢,並且前面的異常,也肯定了Local中必定存在一個主鍵相同的原始數據,因此敢用Single直接獲取。能夠放心的是,這裏並不會走數據庫查詢。

除此以外,還有一個能夠封閉的地方就是關閉了ValidateOnSaveEnabled屬性的SaveChanges方法,可封閉爲以下:

  1. public static int SaveChanges(this DbContext dbContext, bool validateOnSaveEnabled)  
  2.     {  
  3.         bool isReturn = dbContext.Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;  
  4.         try 
  5.         {  
  6.             dbContext.Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;  
  7.             return dbContext.SaveChanges();  
  8.         }  
  9.         finally 
  10.         {  
  11.             if (isReturn)  
  12.             {  
  13.                 dbContext.Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;  
  14.             }  
  15.         }  
  16.     } 

辛苦不是白費的,通過一番折騰,咱們的按需更新實現起來就很是簡單了:

  1. private static void Method09()  
  2.     {  
  3.         Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };  
  4.         using (var db = new DataContext())  
  5.         {  
  6.             //先查詢一次,讓上下文中存在相同主鍵的對象  
  7.             Member oldMember = db.Members.Single(m => m.Id == 1);  
  8.             Console.WriteLine("更新前:{0}。", oldMember.AddDate);  
  9.  
  10.             db.Update<Member>(m => new { m.Password }, member);  
  11.             int count = db.SaveChanges(false);  
  12.             Console.WriteLine("操做結果:{0}", count > 0 ? "更新成功。" : "未更新。");  
  13.  
  14.             Member newMember = db.Members.Single(m => m.Id == 1);  
  15.             Console.WriteLine("更新後:{0}。", newMember.Password);  
  16.         }  
  17.     } 

只須要第10,11行兩行代碼,便可完成完美的按需更新功能。

 

這裏須要特別注意的是,此按需更新的方法只適用於使用新建上下文的環境中,即using(var db = DataContext()){ },由於咱們往上下文中附加了一個非法的實體類(好比上面的member),當提交更改以後,這個非法的實體類依然會存在於上下文中,若是使用這個上下文進行後續的其餘操做,將有可能出現異常。嘗試過在SaveChanges以後將該實體從上下文中移除,跟蹤系統會將該實體變動爲刪除狀態,在下次SaveChanges的時候將之刪除,這個問題本人暫時尚未好的解決方案,在此特別說明。

 

4、源碼獲取

 

本文示例源碼下載:GmfEFUpdateDemo.zip

爲了讓你們能第一時間獲取到本架構的最新代碼,也爲了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網站 http://www.codeplex.com,地址爲:

https://gmframework.codeplex.com/

原文連接:http://www.cnblogs.com/guomingfeng/p/mvc-ef-update.html

相關文章
相關標籤/搜索