EF Code First:數(shù)據(jù)更新最佳實踐
一、前言
最近在整理EntityFramework數(shù)據(jù)更新的代碼,頗有體會,覺得有分享的價值,于是記錄下來,讓需要的人少走些彎路也是好的。
為方便起見,先創(chuàng)建一個控制臺工程,使用using(var db = new DataContext)的形式來一步一步講解EF數(shù)據(jù)更新的可能會遇到的問題及對應(yīng)的解決方案。在獲得最佳方案之后,再整合到本系列的代碼中。
本示例中,用到的數(shù)據(jù)模型如下圖所示:
- 部門:一個部門可有多個角色【1-N】
- 角色:一個角色必有一個部門【N-1】,一個角色可有多個人員【N-N】
- 人員:一個人員可有多個角色【N-N】
并且,我們通過數(shù)據(jù)遷移策略初始化了一些數(shù)據(jù):
初始化數(shù)據(jù)
- protected override void Seed(GmfEFUpdateDemo.Models.DataContext context)
- {
- //部門
- var departments = new []
- {
- new Department {Name = "技術(shù)部"},
- new Department {Name = "財務(wù)部"}
- };
- context.Departments.AddOrUpdate(m => new {m.Name}, departments);
- context.SaveChanges();
- //角色
- var roles = new[]
- {
- new Role{Name = "技術(shù)部經(jīng)理", Department = context.Departments.Single(m=>m.Name =="技術(shù)部")},
- new Role{Name = "技術(shù)總監(jiān)", Department = context.Departments.Single(m=>m.Name =="技術(shù)部")},
- new Role{Name = "技術(shù)人員", Department = context.Departments.Single(m=>m.Name =="技術(shù)部")},
- new Role{Name = "財務(wù)部經(jīng)理", Department = context.Departments.Single(m=>m.Name =="財務(wù)部")},
- new Role{Name = "會計", Department = context.Departments.Single(m=>m.Name =="財務(wù)部")}
- };
- context.Roles.AddOrUpdate(m=>new{m.Name}, roles);
- context.SaveChanges();
- //人員
- var members = new[]
- {
- new Member
- {
- UserName = "郭明鋒",
- Password = "123456",
- Roles = new HashSet<Role>
- {
- context.Roles.Single(m => m.Name == "技術(shù)人員")
- }
- }
- };
- context.Members.AddOrUpdate(m => new {m.UserName}, members);
- context.SaveChanges();
- }
二、整體更新(不考慮更新屬性)
情景一:同一上下文中數(shù)據(jù)取出來更新后直接保存:
代碼:
- private static void Method01()
- {
- using (var db = new DataContext())
- {
- const string userName = "郭明鋒";
- Member oldMember = db.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- oldMember.AddDate = oldMember.AddDate.AddMinutes(10);
- int count = db.SaveChanges();
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新后:{0}。", newMember.AddDate);
- }
- }
代碼解析:操作必然成功,執(zhí)行的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
注意,這里并沒有對更新實體的屬性進(jìn)行篩選,但EF還是聰明的生成了只更新AddDate屬性的sql語句。
情景二:從上下文1中取出數(shù)據(jù)并修改,再在上下文2中進(jìn)行保存:
代碼:
- private static void Method02()
- {
- const string userName = "郭明鋒";
- Member updateMember;
- using (var db1 = new DataContext())
- {
- updateMember = db1.Members.Single(m => m.UserName == userName);
- }
- updateMember.AddDate = DateTime.Now;
- using (var db2 = new DataContext())
- {
- db2.Members.Attach(updateMember);
- DbEntityEntry<Member> entry = db2.Entry(updateMember);
- Console.WriteLine("Attach成功后的狀態(tài):{0}", entry.State); //附加成功之后,狀態(tài)為EntityState.Unchanged
- entry.State = EntityState.Modified;
- int count = db2.SaveChanges();
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db2.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新后:{0}。", newMember.AddDate);
- }
- }
代碼解析:對于db2而言,updateMemner是一個全新的外來的它不認(rèn)識的對象,所以需要使用Attach方法把這個外來對象附加到它的上下文中,Attach之后,實體的對象為 EntityState.Unchanged,如果不改變狀態(tài),在SaveChanged的時候?qū)⑹裁匆膊蛔觥R虼诉€需要把狀態(tài)更改為EntityState.Modified,而由Unchanged -> Modified的改變,是我們強(qiáng)制的,而不是由EF狀態(tài)跟蹤得到的結(jié)果,因而EF無法分辨出哪個屬性變更了,因而將不分青紅皂白地將所有屬性都刷一遍,執(zhí)行如下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
情景三:在情景二的基礎(chǔ)上,上下文2中已存在與外來實體主鍵相同的數(shù)據(jù)了
代碼:
- private static void Method03()
- {
- const string userName = "郭明鋒";
- Member updateMember;
- using (var db1 = new DataContext())
- {
- updateMember = db1.Members.Single(m => m.UserName == userName);
- }
- updateMember.AddDate = DateTime.Now;
- using (var db2 = new DataContext())
- {
- //先查詢一次,讓上下文中存在相同主鍵的對象
- Member oldMember = db2.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- db2.Members.Attach(updateMember);
- DbEntityEntry<Member> entry = db2.Entry(updateMember);
- Console.WriteLine("Attach成功后的狀態(tài):{0}", entry.State); //附加成功之后,狀態(tài)為EntityState.Unchanged
- entry.State = EntityState.Modified;
- int count = db2.SaveChanges();
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db2.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新后:{0}。", newMember.AddDate);
- }
- }
代碼解析:此代碼與情景二相比,就是多了14~16三行代碼,目的是制造一個要更新的數(shù)據(jù)在上下文2中正在使用的場景,這時會發(fā)生什么情況呢?
當(dāng)代碼執(zhí)行到18行的Attach的時候,將引發(fā)一個EF數(shù)據(jù)更新時非常常見的異常:
- 捕捉到 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中已經(jīng)有了一個相同主鍵的對象,不能再附加了。
這應(yīng)該是一個非常常見的場景,也就是必須想辦法解決的場景。其實只要獲得現(xiàn)有實體數(shù)據(jù)的跟蹤,再把新數(shù)據(jù)賦到現(xiàn)有實體上,就可以解決問題了,此方法唯一的缺點就是要獲取到舊的實體數(shù)據(jù)。代碼如下:
- private static void Method04()
- {
- const string userName = "郭明鋒";
- Member updateMember;
- using (var db1 = new DataContext())
- {
- updateMember = db1.Members.Single(m => m.UserName == userName);
- }
- updateMember.AddDate = DateTime.Now;
- using (var db2 = new DataContext())
- {
- //先查詢一次,讓上下文中存在相同主鍵的對象
- Member oldMember = db2.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- DbEntityEntry<Member> entry = db2.Entry(oldMember);
- entry.CurrentValues.SetValues(updateMember);
- int count = db2.SaveChanges();
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db2.Members.Single(m => m.UserName == userName);
- Console.WriteLine("更新后:{0}。", newMember.AddDate);
- }
- }
代碼中的18~19行是核心代碼,先從上下文中的舊實體獲取跟蹤,第19行的SetValues方法就是把新值設(shè)置到舊實體上(這一條很強(qiáng)大,支持任何類型,比如ViewObject,DTO與POCO可以直接映射傳值)。由于值的更新是直接在上下文中的現(xiàn)有實體上進(jìn)行的,EF會自己跟蹤值的變化,因此這里并不需要我們來強(qiáng)制設(shè)置狀態(tài)為Modified,執(zhí)行的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
#p#
整體更新的最佳實現(xiàn)
綜合上面的幾種情景,我們可以得到EF對實體整體更新的最佳方案,這里寫成DbContext的擴(kuò)展方法,代碼如下:
- public static void Update<TEntity>(this DbContext dbContext, params TEntity[] entities) where TEntity : EntityBase
- {
- if (dbContext == null) throw new ArgumentNullException("dbContext");
- if (entities == null) throw new ArgumentNullException("entities");
- foreach (TEntity entity in entities)
- {
- DbSet<TEntity> dbSet = dbContext.Set<TEntity>();
- try
- {
- DbEntityEntry<TEntity> entry = dbContext.Entry(entity);
- if (entry.State == EntityState.Detached)
- {
- dbSet.Attach(entity);
- entry.State = EntityState.Modified;
- }
- }
- catch (InvalidOperationException)
- {
- TEntity oldEntity = dbSet.Find(entity.Id);
- dbContext.Entry(oldEntity).CurrentValues.SetValues(entity);
- }
- }
- }
調(diào)用代碼如下:
- db.Update<Member>(member);
- int count = db.SaveChanges();
針對不同的情景,將執(zhí)行不同的行為:
- 情景一:上面代碼第11行執(zhí)行后entry.State將為EntityState.Modified,會直接退出此Update方法直接進(jìn)入SaveChanges的執(zhí)行。此情景執(zhí)行的sql語句為只更新變更的實體屬性。
- 情景二:將正確執(zhí)行 try 代碼塊。此情景執(zhí)行的sql語句為更新全部實體屬性。
- 情景三:在代碼執(zhí)行到第12行的Attach方法時將拋出 InvalidOperationException 異常,接著執(zhí)行 catch 代碼塊。此情景執(zhí)行的sql語句為只更新變更的實體屬性。
三、按需更新(更新指定實體屬性)
需求分析
前面已經(jīng)有整體更新了,很多時候也都能做到只更新變化的實體屬性,為什么還要來個“按需更新”的需求呢?主要基于以下幾點理由:
- 整體更新中獲取數(shù)據(jù)的變更是要把新值與原始值的屬性一一對比的,因而整體更新要從數(shù)據(jù)庫中獲取完整的實體數(shù)據(jù),以保證被更新的只有我們想要改變的實體屬性,這樣進(jìn)行整體更新時至少要從數(shù)據(jù)庫中查詢一次數(shù)據(jù)。
- 執(zhí)行的更新語句有可能是更新所有實體屬性的(如上的情景三),如果實體屬性很多,就容易造成計算資源的浪費(因為我們只需要更新其中的某幾個屬性值)。
- 不能只更新指定的實體屬性,有了按需更新,我們可以非常方便的只更新指定的屬性,沒有指定的屬性即使值變化了也不更新
需求實現(xiàn)
按需更新,也就是知道要更新的實體屬性,比如用戶要修改密碼,就只是要把Password這個屬性的值變更為指定的新值,其他的最好是盡量不驚動。當(dāng)然,至少還是要知道要更新數(shù)據(jù)的主鍵的,否則,更新對象就不明了。下面就以設(shè)置密碼為例來說明問題。
要設(shè)置密碼,我構(gòu)造了一個空的Member類來裝載新密碼:
- Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second};
然后,我們想當(dāng)然的寫出了如下實現(xiàn)代碼:
- private static void Method06()
- {
- Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second};
- using (var db = new DataContext())
- {
- DbEntityEntry<Member> entry = db.Entry(member);
- entry.State = EntityState.Unchanged;
- entry.Property("Password").IsModified = true;
- int count = db.SaveChanges();
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新后:{0}。", newMember.Password);
- }
- }
然后,在執(zhí)行第9行SaveChanges的時候引發(fā)了如下異常:
- 捕捉到 System.Data.Entity.Validation.DbEntityValidationException
- HResult=-2146232032
- Message=對一個或多個實體的驗證失敗。有關(guān)詳細(xì)信息,請參見“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:
為什么出現(xiàn)此異常?因為前面我們創(chuàng)建的Member對象只包含一個Id,一個Password屬性,其他的屬性并沒有賦值,也不考慮是否規(guī)范,這樣就定義出了一個不符合實體類驗證定義的對象了(Member類要求UserName屬性是不可為空的)。幸好,DbContext.Configuration中給我們定義了是否在保存時驗證實體有效性(ValidateOnSaveEnabled)這個開關(guān),我們只要在執(zhí)行按需更新的保存時把驗證閉,在保存成功后再開啟即可,更改代碼如下:
- private static void Method06()
- {
- Member member = new Member {Id = 1, Password = "NewPassword" + DateTime.Now.Second};
- using (var db = new DataContext())
- {
- DbEntityEntry<Member> entry = db.Entry(member);
- entry.State = EntityState.Unchanged;
- entry.Property("Password").IsModified = true;
- db.Configuration.ValidateOnSaveEnabled = false;
- int count = db.SaveChanges();
- db.Configuration.ValidateOnSaveEnabled = true;
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新后:{0}。", newMember.Password);
- }
- }
與整體更新一樣,理所當(dāng)然的會出現(xiàn)當(dāng)前上下文已經(jīng)存在了相同主鍵的實體數(shù)據(jù)的情況,當(dāng)然,根據(jù)之前的經(jīng)驗,也很容易的進(jìn)行處理了:
- private static void Method07()
- {
- Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };
- using (var db = new DataContext())
- {
- //先查詢一次,讓上下文中存在相同主鍵的對象
- Member oldMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- try
- {
- DbEntityEntry<Member> entry = db.Entry(member);
- entry.State = EntityState.Unchanged;
- entry.Property("Password").IsModified = true;
- }
- catch (InvalidOperationException)
- {
- DbEntityEntry<Member> entry = db.Entry(oldMember);
- entry.CurrentValues.SetValues(member);
- entry.State = EntityState.Unchanged;
- entry.Property("Password").IsModified = true;
- }
- db.Configuration.ValidateOnSaveEnabled = false;
- int count = db.SaveChanges();
- db.Configuration.ValidateOnSaveEnabled = true;
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新后:{0}。", newMember.Password);
- }
- }
但是,上面的代碼卻無法正常工作,經(jīng)過調(diào)試發(fā)現(xiàn),當(dāng)執(zhí)行到第20行的時候,entry中跟蹤的數(shù)據(jù)又變回oldMember了,經(jīng)過一番EntityFramework源碼搜索,終于找到了問題的出處(System.Data.Entity.Internal.InternalEntityEntry類中):
- public EntityState State
- {
- get
- {
- if (!this.IsDetached)
- return this._stateEntry.State;
- else
- return EntityState.Detached;
- }
- set
- {
- if (!this.IsDetached)
- {
- if (this._stateEntry.State == EntityState.Modified && value == EntityState.Unchanged)
- this.CurrentValues.SetValues(this.OriginalValues);
- this._stateEntry.ChangeState(value);
- }
- else
- {
- switch (value)
- {
- case EntityState.Unchanged:
- this._internalContext.Set(this._entityType).InternalSet.Attach(this._entity);
- break;
- case EntityState.Added:
- this._internalContext.Set(this._entityType).InternalSet.Add(this._entity);
- break;
- case EntityState.Deleted:
- case EntityState.Modified:
- this._internalContext.Set(this._entityType).InternalSet.Attach(this._entity);
- this._stateEntry = this._internalContext.GetStateEntry(this._entity);
- this._stateEntry.ChangeState(value);
- break;
- }
- }
- }
- }
第14、15行,當(dāng)狀態(tài)由Modified更改為Unchanged的時候,又把數(shù)據(jù)重新設(shè)置為舊的數(shù)據(jù)OriginalValues了。真吭!
好吧,看來在DbContext中折騰已經(jīng)沒戲了,只要去它老祖宗ObjectContext中找找出路,更改實現(xiàn)如下:
- private static void Method08()
- {
- Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };
- using (var db = new DataContext())
- {
- //先查詢一次,讓上下文中存在相同主鍵的對象
- Member oldMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- try
- {
- DbEntityEntry<Member> entry = db.Entry(member);
- entry.State = EntityState.Unchanged;
- entry.Property("Password").IsModified = true;
- }
- catch (InvalidOperationException)
- {
- ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;
- ObjectStateEntry objectEntry = objectContext.ObjectStateManager.GetObjectStateEntry(oldMember);
- objectEntry.ApplyCurrentValues(member);
- objectEntry.ChangeState(EntityState.Unchanged);
- objectEntry.SetModifiedProperty("Password");
- }
- db.Configuration.ValidateOnSaveEnabled = false;
- int count = db.SaveChanges();
- db.Configuration.ValidateOnSaveEnabled = true;
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新后:{0}。", newMember.Password);
- }
- }
catch代碼塊使用了EF4.0時代使用的ObjectContext來實現(xiàn),很好的達(dá)到了我們的目的,執(zhí)行的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
#p#
封裝重構(gòu)的分析
以上的實現(xiàn)中,屬性名都是以硬編碼的形式直接寫到實現(xiàn)類中,作為底層的封閉,這是肯定不行的,至少也要作為參數(shù)傳遞到一個通用的更新方法中。參照整體更新的擴(kuò)展方法定義,我們很容易的就能定義出如下簽名的擴(kuò)展方法:
- public static void Update<TEntity>(this DbContext dbContext, string[] propertyNames, params TEntity[] entities) where TEntity : EntityBase
- 方法調(diào)用方式:
- dbContext.Update<Member>(new[] {"Password"}, member);
調(diào)用中屬性名依然要使用字符串的方式,寫起來麻煩,還容易出錯。看來,強(qiáng)類型才是最好的選擇。
寫到這,突然想起了做數(shù)據(jù)遷移的時候使用到的System.Data.Entity.Migrations.IDbSetExtensions 類中的擴(kuò)展方法
- public static void AddOrUpdate<TEntity>(this IDbSet<TEntity> set, Expression<Func<TEntity, object>> identifierExpression, params TEntity[] entities) where TEntity : class
其中的參數(shù)Expression<Func<TEntity, object>> identifierExpression就是用于傳送實體屬性名的,于是,我們參照著,可以定義出如下簽名的更新方法:
- public static void Update<TEntity>(this DbContext dbContext, Expression<Func<TEntity, object>> propertyExpression, params TEntity[] entities) where TEntity : EntityBase
- 方法調(diào)用方式:
- db.Update<Member>(m => new { m.Password }, member);
到這里,如何從Expression<Func<TEntity, object>>獲得屬性名成為了完成封閉的關(guān)鍵。還是經(jīng)過調(diào)試,有了如下發(fā)現(xiàn):
運行時的Expression表達(dá)式中,Body屬性中有個類型為ReadOnlyCollection<MemberInfo> 的 Members集合屬性,我們需要的屬性正以MemberInfo的形式存在其中,因此,我們借助一下 dynamic 類型,將Members屬性解析出來,即可輕松得到我們想的數(shù)據(jù)。
- ReadOnlyCollection<MemberInfo> memberInfos = ((dynamic)propertyExpression.Body).Members;
按需更新的最佳實現(xiàn)
經(jīng)過上面的分析,難點已逐個擊破,很輕松的就得到了如下擴(kuò)展方法的實現(xiàn):
- public static void Update<TEntity>(this DbContext dbContext, Expression<Func<TEntity, object>> propertyExpression, params TEntity[] entities)
- where TEntity : EntityBase
- {
- if (propertyExpression == null) throw new ArgumentNullException("propertyExpression");
- if (entities == null) throw new ArgumentNullException("entities");
- ReadOnlyCollection<MemberInfo> memberInfos = ((dynamic)propertyExpression.Body).Members;
- foreach (TEntity entity in entities)
- {
- try
- {
- DbEntityEntry<TEntity> entry = dbContext.Entry(entity);
- entry.State = EntityState.Unchanged;
- foreach (var memberInfo in memberInfos)
- {
- entry.Property(memberInfo.Name).IsModified = true;
- }
- }
- catch (InvalidOperationException)
- {
- TEntity originalEntity = dbContext.Set<TEntity>().Local.Single(m => m.Id == entity.Id);
- ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
- ObjectStateEntry objectEntry = objectContext.ObjectStateManager.GetObjectStateEntry(originalEntity);
- objectEntry.ApplyCurrentValues(entity);
- objectEntry.ChangeState(EntityState.Unchanged);
- foreach (var memberInfo in memberInfos)
- {
- objectEntry.SetModifiedProperty(memberInfo.Name);
- }
- }
- }
- }
注意,這里的第20行雖然進(jìn)行了原始數(shù)據(jù)的查詢,但是從DbSet<T>.Local中進(jìn)行的查詢,而且前面的異常,也確定了Local中一定存在一個主鍵相同的原始數(shù)據(jù),所以敢用Single直接獲取。可以放心的是,這里并不會走數(shù)據(jù)庫查詢。
除此之外,還有一個可以封閉的地方就是關(guān)閉了ValidateOnSaveEnabled屬性的SaveChanges方法,可封閉為如下:
- public static int SaveChanges(this DbContext dbContext, bool validateOnSaveEnabled)
- {
- bool isReturn = dbContext.Configuration.ValidateOnSaveEnabled != validateOnSaveEnabled;
- try
- {
- dbContext.Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled;
- return dbContext.SaveChanges();
- }
- finally
- {
- if (isReturn)
- {
- dbContext.Configuration.ValidateOnSaveEnabled = !validateOnSaveEnabled;
- }
- }
- }
辛苦不是白費的,經(jīng)過一番折騰,我們的按需更新實現(xiàn)起來就非常簡單了:
- private static void Method09()
- {
- Member member = new Member { Id = 1, Password = "NewPassword" + DateTime.Now.Second };
- using (var db = new DataContext())
- {
- //先查詢一次,讓上下文中存在相同主鍵的對象
- Member oldMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新前:{0}。", oldMember.AddDate);
- db.Update<Member>(m => new { m.Password }, member);
- int count = db.SaveChanges(false);
- Console.WriteLine("操作結(jié)果:{0}", count > 0 ? "更新成功。" : "未更新。");
- Member newMember = db.Members.Single(m => m.Id == 1);
- Console.WriteLine("更新后:{0}。", newMember.Password);
- }
- }
只需要第10,11行兩行代碼,即可完成完美的按需更新功能。
這里需要特別注意的是,此按需更新的方法只適用于使用新建上下文的環(huán)境中,即using(var db = DataContext()){ },因為我們往上下文中附加了一個非法的實體類(比如上面的member),當(dāng)提交更改之后,這個非法的實體類依然會存在于上下文中,如果使用這個上下文進(jìn)行后續(xù)的其他操作,將有可能出現(xiàn)異常。嘗試過在SaveChanges之后將該實體從上下文中移除,跟蹤系統(tǒng)會將該實體變更為刪除狀態(tài),在下次SaveChanges的時候?qū)⒅畡h除,這個問題本人暫時還沒有好的解決方案,在此特別說明。 |
四、源碼獲取
本文示例源碼下載:GmfEFUpdateDemo.zip
為了讓大家能第一時間獲取到本架構(gòu)的最新代碼,也為了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網(wǎng)站 http://www.codeplex.com,地址為:
https://gmframework.codeplex.com/
原文鏈接:http://www.cnblogs.com/guomingfeng/p/mvc-ef-update.html