EF Code First:数据更新最佳实践

EF Code First:数据更新最佳实践

最近在整理EntityFramework数据更新的代码,颇有体会,觉得有分享的价值,于是记录下来,让需要的人少走些弯路也是好的。为方便起见,先创建一个控制台工程,使用using(var db = new DataContext)的形式来一步一步讲解EF数据更新的可能会遇到的问题及对应的解决方案。在获得最佳方案之后,再整合到本系列的代码中。

一、前言

最近在整理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. var roles = new[]
  13. {
  14. new Role{Name = "技术部经理", Department = context.Departments.Single(m=>m.Name =="技术部")},
  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. };
  20. context.Roles.AddOrUpdate(m=>new{m.Name}, roles);
  21. context.SaveChanges();
  22. //人员
  23. var members = new[]
  24. {
  25. new Member
  26. {
  27. UserName = "郭明锋",
  28. Password = "123456",
  29. Roles = new HashSet<Role>
  30. {
  31. context.Roles.Single(m => m.Name == "技术人员")
  32. }
  33. }
  34. };
  35. context.Members.AddOrUpdate(m => new {m.UserName}, members);
  36. context.SaveChanges();
  37. }

二、整体更新(不考虑更新属性)

情景一:同一上下文中数据取出来更新后直接保存:

代码:

  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. oldMember.AddDate = oldMember.AddDate.AddMinutes(10);
  9. int count = db.SaveChanges();
  10. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  11. Member newMember = db.Members.Single(m => m.UserName == userName);
  12. Console.WriteLine("更新后:{0}。", newMember.AddDate);
  13. }
  14. }

代码解析:操作必然成功,执行的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. Member updateMember;
  5. using (var db1 = new DataContext())
  6. {
  7. updateMember = db1.Members.Single(m => m.UserName == userName);
  8. }
  9. updateMember.AddDate = DateTime.Now;
  10. using (var db2 = new DataContext())
  11. {
  12. db2.Members.Attach(updateMember);
  13. DbEntityEntry<Member> entry = db2.Entry(updateMember);
  14. Console.WriteLine("Attach成功后的状态:{0}", entry.State); //附加成功之后,状态为EntityState.Unchanged
  15. entry.State = EntityState.Modified;
  16. int count = db2.SaveChanges();
  17. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  18. Member newMember = db2.Members.Single(m => m.UserName == userName);
  19. Console.WriteLine("更新后:{0}。", newMember.AddDate);
  20. }
  21. }

代码解析:对于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. Member updateMember;
  5. using (var db1 = new DataContext())
  6. {
  7. updateMember = db1.Members.Single(m => m.UserName == userName);
  8. }
  9. updateMember.AddDate = DateTime.Now;
  10. using (var db2 = new DataContext())
  11. {
  12. //先查询一次,让上下文中存在相同主键的对象
  13. Member oldMember = db2.Members.Single(m => m.UserName == userName);
  14. Console.WriteLine("更新前:{0}。", oldMember.AddDate);
  15. db2.Members.Attach(updateMember);
  16. DbEntityEntry<Member> entry = db2.Entry(updateMember);
  17. Console.WriteLine("Attach成功后的状态:{0}", entry.State); //附加成功之后,状态为EntityState.Unchanged
  18. entry.State = EntityState.Modified;
  19. int count = db2.SaveChanges();
  20. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  21. Member newMember = db2.Members.Single(m => m.UserName == userName);
  22. Console.WriteLine("更新后:{0}。", newMember.AddDate);
  23. }
  24. }

代码解析:此代码与情景二相比,就是多了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. Member updateMember;
  5. using (var db1 = new DataContext())
  6. {
  7. updateMember = db1.Members.Single(m => m.UserName == userName);
  8. }
  9. updateMember.AddDate = DateTime.Now;
  10. using (var db2 = new DataContext())
  11. {
  12. //先查询一次,让上下文中存在相同主键的对象
  13. Member oldMember = db2.Members.Single(m => m.UserName == userName);
  14. Console.WriteLine("更新前:{0}。", oldMember.AddDate);
  15. DbEntityEntry<Member> entry = db2.Entry(oldMember);
  16. entry.CurrentValues.SetValues(updateMember);
  17. int count = db2.SaveChanges();
  18. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  19. Member newMember = db2.Members.Single(m => m.UserName == userName);
  20. Console.WriteLine("更新后:{0}。", newMember.AddDate);
  21. }
  22. }

代码中的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. foreach (TEntity entity in entities)
  6. {
  7. DbSet<TEntity> dbSet = dbContext.Set<TEntity>();
  8. try
  9. {
  10. DbEntityEntry<TEntity> entry = dbContext.Entry(entity);
  11. if (entry.State == EntityState.Detached)
  12. {
  13. dbSet.Attach(entity);
  14. entry.State = EntityState.Modified;
  15. }
  16. }
  17. catch (InvalidOperationException)
  18. {
  19. TEntity oldEntity = dbSet.Find(entity.Id);
  20. dbContext.Entry(oldEntity).CurrentValues.SetValues(entity);
  21. }
  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语句为只更新变更的实体属性。

三、按需更新(更新指定实体属性)

需求分析

前面已经有整体更新了,很多时候也都能做到只更新变化的实体属性,为什么还要来个“按需更新”的需求呢?主要基于以下几点理由:

  • 整体更新中获取数据的变更是要把新值与原始值的属性一一对比的,因而整体更新要从数据库中获取完整的实体数据,以保证被更新的只有我们想要改变的实体属性,这样进行整体更新时至少要从数据库中查询一次数据
  • 执行的更新语句有可能是更新所有实体属性的(如上的情景三),如果实体属性很多,就容易造成计算资源的浪费(因为我们只需要更新其中的某几个属性值)。
  • 不能只更新指定的实体属性,有了按需更新,我们可以非常方便的只更新指定的属性,没有指定的属性即使值变化了也不更新

需求实现

按需更新,也就是知道要更新的实体属性,比如用户要修改密码,就只是要把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. Member newMember = db.Members.Single(m => m.Id == 1);
  12. Console.WriteLine("更新后:{0}。", newMember.Password);
  13. }
  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. Member newMember = db.Members.Single(m => m.Id == 1);
  14. Console.WriteLine("更新后:{0}。", newMember.Password);
  15. }
  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. try
  10. {
  11. DbEntityEntry<Member> entry = db.Entry(member);
  12. entry.State = EntityState.Unchanged;
  13. entry.Property("Password").IsModified = true;
  14. }
  15. catch (InvalidOperationException)
  16. {
  17. DbEntityEntry<Member> entry = db.Entry(oldMember);
  18. entry.CurrentValues.SetValues(member);
  19. entry.State = EntityState.Unchanged;
  20. entry.Property("Password").IsModified = true;
  21. }
  22. db.Configuration.ValidateOnSaveEnabled = false;
  23. int count = db.SaveChanges();
  24. db.Configuration.ValidateOnSaveEnabled = true;
  25. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  26. Member newMember = db.Members.Single(m => m.Id == 1);
  27. Console.WriteLine("更新后:{0}。", newMember.Password);
  28. }
  29. }

但是,上面的代码却无法正常工作,经过调试发现,当执行到第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. try
  10. {
  11. DbEntityEntry<Member> entry = db.Entry(member);
  12. entry.State = EntityState.Unchanged;
  13. entry.Property("Password").IsModified = true;
  14. }
  15. catch (InvalidOperationException)
  16. {
  17. ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;
  18. ObjectStateEntry objectEntry = objectContext.ObjectStateManager.GetObjectStateEntry(oldMember);
  19. objectEntry.ApplyCurrentValues(member);
  20. objectEntry.ChangeState(EntityState.Unchanged);
  21. objectEntry.SetModifiedProperty("Password");
  22. }
  23. db.Configuration.ValidateOnSaveEnabled = false;
  24. int count = db.SaveChanges();
  25. db.Configuration.ValidateOnSaveEnabled = true;
  26. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  27. Member newMember = db.Members.Single(m => m.Id == 1);
  28. Console.WriteLine("更新后:{0}。", newMember.Password);
  29. }
  30. }

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. 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. db.Update<Member>(m => new { m.Password }, member);
  10. int count = db.SaveChanges(false);
  11. Console.WriteLine("操作结果:{0}", count > 0 ? "更新成功。" : "未更新。");
  12. Member newMember = db.Members.Single(m => m.Id == 1);
  13. Console.WriteLine("更新后:{0}。", newMember.Password);
  14. }
  15. }

只需要第10,11行两行代码,即可完成完美的按需更新功能。

这里需要特别注意的是,此按需更新的方法只适用于使用新建上下文的环境中,即using(var db = DataContext()){ },因为我们往上下文中附加了一个非法的实体类(比如上面的member),当提交更改之后,这个非法的实体类依然会存在于上下文中,如果使用这个上下文进行后续的其他操作,将有可能出现异常。尝试过在SaveChanges之后将该实体从上下文中移除,跟踪系统会将该实体变更为删除状态,在下次SaveChanges的时候将之删除,这个问题本人暂时还没有好的解决方案,在此特别说明。

四、源码获取

本文示例源码下载:GmfEFUpdateDemo.zip

为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:

https://gmframework.codeplex.com/

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

时间: 2024-11-02 06:04:04

EF Code First:数据更新最佳实践的相关文章

不容错过,Code Review 的最佳实践方案来了

前言 我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题.包括像Google.微软这些公司,Code Review都是基本要求,代码合并之前必须要有人审查通过才行. 然而对于我观察到的大部分软件开发团队来说,认真做Code Review的很少,有的流于形式,有的可能根本就没有Code Review的环节,代码质量只依赖于事后的测试.也有些团队想做好代码审查,但不知道怎么做比较好.网上关于如何做Code Review的文章

转 Code Review最佳实践

本文转自 https://www.cnblogs.com/dotey/p/11216430.html 我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题.包括像Google.微软这些公司,Code Review都是基本要求,代码合并之前必须要有人审查通过才行. 然而对于我观察到的大部分软件开发团队来说,认真做Code Review的很少,有的流于形式,有的可能根本就没有Code Review的环节,代码质量只依赖于事后的

MVC项目实践,在三层架构下实现SportsStore-01,EF Code First建模、DAL层等

http://www.cnblogs.com/darrenji/p/3809219.html 本篇为系列第一篇,包括: ■ 1.搭建项目■ 2.卸载Entity Framework组件,并安装最新版本■ 3.使用EF Code First创建领域模型和EF上下文■ 4.三层架构设计    □ 4.1 创建DAL层        ※ 4.1.1 MySportsStore.IDAL详解        ※ 4.1.2 MySportsStore.DAL详解 1.搭建项目 MySportsStore.

Code Review最佳实践

Code Review最佳实践 原文链接 : Code Review Best Practices 原文作者 : Kevin London 译文出自 : 开发技术前线 www.devtf.cn 译者 : ayyb1988 校对者: chaossss 状态 : 完成 在Wiredrive上,我们做了很多的Code Review.在此之前我从来没有做过,这对于我来说是一个全新的体验,下面来总结一下在Code Review中做的事情以及说说Code Review的最好方式. 简单的说,Code Rev

ASP.NET Web API实践系列02,在MVC4下的一个实例, 包含EF Code First,依赖注入, Bootstrap等

本篇体验在MVC4下,实现一个对Book信息的管理,包括增删查等,用到了EF Code First, 使用Unity进行依赖注入,前端使用Bootstrap美化.先上最终效果: →创建一个MVC4项目,选择Web API模版. →在Models文件夹创建一个Book.cs类. namespace MyMvcAndWebApi.Models { public class Book { public int Id { get; set; } public string Name { get; set

App 后台架构设计方案 设计思想与最佳实践

转载请注明出处:http://blog.csdn.net/smartbetter/article/details/53933096 做App做的久了,就想研究一下与之相关的App后台,发现也是蛮有趣的.App后台的两个重要作用就是 远程存储数据 和 消息中转.这里面的知识体系也是相当复杂,做好一个App后台也是需要长期锤炼的.本篇文章从 App 后台架构 的角度介绍.好了,下面进入正题: 说起架构,我们先看一下何为架构,百度百科是这样说的:架构,又名软件架构,是有关软件整体结构与组件的抽象描述,

领域驱动设计之单元测试最佳实践(二)

领域驱动设计之单元测试最佳实践(一) 介绍完了DDD案例,我们终于可以进入主题了,本方案的测试代码基于Xunit编写,断言组件采用了FluentAssertions,类似的组件还有Shouldly.另外本案例使用了Code Contracts for .NET,如果不安装此插件,可能有个别测试不能正确Pass. 为了实现目标中的第二点:"尽量不Mock,包括数据库读取部分”,我尝试过3种方案: 1.测试代码连接真实数据库,只需要将测试数据库配置到测试项目中的web.config中,即可达到这一目

领域驱动设计之单元测试最佳实践(一)

领域驱动设计之单元测试最佳实践(二) 一直以来,我试图找到一种有效的单元测试模式,使得“单元测试”真正能够在团队中流行起来,让单元测试不再是走过场,而是让单元测试切切实实成为提高代码质量的途径. 本文将描述一种以EF Code First模式实现的领域驱动项目实施单元测试的方案. 在描述这一方案之前,让我们看看这一最佳实践源于何种考虑和最终实现的目标: 1.以MVC项目为例,如果将单元测试的重心放在如何测试一个Controller或Action将收效甚微,原因有二: 从原则上讲Controlle

memcache的最佳实践方案

1.memcached的基本设置 1)启动Memcache的服务器端 # /usr/local/bin/memcached -d -m 10 -u root -l 192.168.0.200 -p 12000 -c 256 -P /tmp/memcached.pid -d选项是启动一个守护进程, -m是分配给Memcache使用的内存数量,单位是MB,我这里是10MB, -u是运行Memcache的用户,我这里是root, -l是监听的服务器IP地址,如果有多个地址的话,我这里指定了服务器的IP