MVC實用構架設計(三)——EF-Code First(6):數據更新最佳實踐


前言

  最近在整理EntityFramework數據更新的代碼,頗有體會,覺得有分享的價值,於是記錄下來,讓需要的人少走些彎路也是好的。

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

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

  

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

  並且,我們通過數據遷移策略初始化了一些數據:

 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();
39 }
初始化數據

 

整體更新(不考慮更新屬性)

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

  代碼:

 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     }
16 }

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

exec sp_executesql N'update [dbo].[Members]
set [AddDate] = @0
where ([Id] = @1)
',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     }
24 }

  代碼解析:對於db2而言,updateMemner是一個全新的外來的它不認識的對象,所以需要使用Attach方法把這個外來對象附加到它的上下文中,Attach之后,實體的對象為 EntityState.Unchanged,如果不改變狀態,在SaveChanged的時候將什么也不做。因此還需要把狀態更改為EntityState.Modified,而由Unchanged -> Modified的改變,是我們強制的,而不是由EF狀態跟蹤得到的結果,因而EF無法分辨出哪個屬性變更了,因而將不分青紅皂白地將所有屬性都刷一遍,執行如下sql語句:

exec sp_executesql N'update [dbo].[Members]
set [UserName] = @0, [Password] = @1, [AddDate] = @2, [IsDeleted] = @3
where ([Id] = @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     }
28 }

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

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

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

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

exec sp_executesql N'update [dbo].[Members]
set [AddDate] = @0
where ([Id] = @1)
',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     }
24 }

  調用代碼如下:

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語句為只更新變更的實體屬性。 

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

需求分析

  前面已經有整體更新了,很多時候也都能做到只更新變化的實體屬性,為什么還要來個“按需更新”的需求呢?主要基於以下幾點理由:

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

需求實現

  按需更新,也就是知道要更新的實體屬性,比如用戶要修改密碼,就只是要把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     }
15 }

   然后,在執行第9行SaveChanges的時候引發了如下異常:

捕捉到 System.Data.Entity.Validation.DbEntityValidationException
  HResult=-2146232032
  Message=對一個或多個實體的驗證失敗。有關詳細信息,請參見“EntityValidationErrors”屬性。
  Source=EntityFramework
  StackTrace:
       在 System.Data.Entity.Internal.InternalContext.SaveChanges()
       在 System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
       在 System.Data.Entity.DbContext.SaveChanges()
       在 GmfEFUpdateDemo.Program.Method06() 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 224
       在 GmfEFUpdateDemo.Program.Main(String[] args) 位置 d:\Documents\Visual Studio 2012\Projects\GmfEFUpdateDemo\GmfEFUpdateDemo\Program.cs:行號 63
  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     }
17 }

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

 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     }

  第14、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語句如下:

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

 封裝重構的分析

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

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

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

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

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就是用於傳送實體屬性名的,於是,我們參照着,可以定義出如下簽名的更新方法:

public static void Update<TEntity>(this DbContext dbContext, Expression<Func<TEntity, object>> propertyExpression, params TEntity[] entities) where TEntity : EntityBase

方法調用方式:
db.Update<Member>(m => new { m.Password }, member);

  到這里,如何從Expression<Func<TEntity, object>>獲得屬性名成為了完成封閉的關鍵。還是經過調試,有了如下發現:

  

  運行時的Expression表達式中,Body屬性中有個類型為ReadOnlyCollection<MemberInfo> 的 Members集合屬性,我們需要的屬性正以MemberInfo的形式存在其中,因此,我們借助一下 dynamic 類型,將Members屬性解析出來,即可輕松得到我們想的數據。

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的時候將之刪除,這個問題本人暫時還沒有好的解決方案,在此特別說明。

源碼獲取

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

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

  https://gmframework.codeplex.com/

  可以通過下列途徑獲取到最新代碼:

  • 如果你是本項目的參與者,可以通過VS自帶的團隊TFS直接連接到 https://tfs.codeplex.com:443/tfs/TFS17 獲取最新代碼
  • 如果你安裝有SVN客戶端(親測TortoiseSVN 1.6.7可用),可以連接到 https://gmframework.svn.codeplex.com/svn 獲取最新代
  • 如果以上條件都不滿足,你可以進入頁面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代碼,也可以點擊頁面上的 Download 鏈接進行壓縮包的下載,你還可以點擊頁面上的 History 鏈接獲取到歷史版本的源代碼
  • 如果你想和大家一起學習MVC,學習EF,歡迎加入群:5008599(群發言僅限技術討論,拒絕閑聊,拒絕醬油,拒絕廣告)
  • 如果你想與我共同來完成這個開源項目,可以隨時聯系我。

系列導航

  1. MVC實用架構設計(〇)——總體設計
  2. MVC實用架構設計(一)——項目結構搭建
  3. MVC實用架構設計(二)——使用MEF實用IOC
  4. MVC實用架構設計(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
  5. MVC實用架構設計(三)——EF-Code First(2):實體映射、數據遷移,重構
  6. MVC實用架構設計(三)——EF-Code First(3):使用T4模板生成相似代碼
  7. MVC實用架構設計(三)——EF-Code First(4):數據查詢
  8. MVC實用架構設計(三)——EF-Code First(5):二級緩存
  9. MVC實體架構設計(三)——EF-COde First(6):數據更新
  10. 未完待續。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM