EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态...

EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态
原文:EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态

本文目录

  • 查看实体当前、原始和数据库值:DbEntityEntry
  • 查看实体的某个属性值:GetValue<TValue>方法
  • 拷贝DbPropertyValues到实体:ToObject方法
  • 修改DbPropertyValues当前值:索引器
  • 克隆实体:Clone方法
  • 设置实体的值:SetValues方法
  • 克隆实体:SetValues
  • 获取和设置实体的单个属性:Property方法
  • 查询实体的属性是否被修改:IsModified方法
  • 修改导航属性
  • 重新加载实体:Reload方法
  • 读取相关联的实体和状态:DbContext.ChangeTracker.Entries方法
  • EF里如何解决更新时的冲突
  • 重写上下文的SaveChanges方法记录结果集里实体的各种增/删/改
  • 本文源码和系列文章导航

文章开始前建议大家为了更好的记忆最好自己实现文中的所有方法。如果非要直接运行我的demo,必要的时候需要恢复下数据库数据,否则找不到记录。

之前的章节已经演示了context.Entry方法可以拿到实体的状态(EntityState),来看一个方法:

        /// <summary>/// 单个实体的状态/// </summary>private static void PrintState(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var canyon = (from d in context.Destinationswhere d.Name == "Grand Canyon"select d).Single();DbEntityEntry<DbContexts.Model.Destination> entry = context.Entry(canyon);Console.WriteLine("Before Edit:{0}", entry.State);   //Unchaged
canyon.TravelWarnings = "Take a lot of Water!";DbEntityEntry<DbContexts.Model.Destination> entrys = context.Entry(canyon);

Console.WriteLine(
"After Edit:{0}", entrys.State); //Modified }}

context.Entry方法有两个重载,分别返回泛型DbEntityEntry<TEntity>和非泛型的DbEntityEntry,它们都可以监测到实体的状态,并且通过DbEntityEntry还可以操作实体的当前值、原始值和数据库值。分别是:

  • 当前值(Current Value):程序里设置实体属性的值(在内存中,还没提交数据库);
  • 原始值(Original Value):被数据库上下文跟踪到时的值(程序取出数据库的值,可能不是最新的);
  • 数据库值(Database Value):数据库里的值(此时此刻数据库里最新的值)

来看一个例子:

        /// <summary>/// 打印实体当前、原始和数据库值/// </summary>private static void PrintLodgingInfo(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();hotel.Name = "Super Grand Hotel";context.Database.ExecuteSqlCommand(@"UPDATE Lodgings SET Name = 'Not-So-Grand Hotel' WHERE Name = 'Grand Hotel'");PrintChangeTrackingInfo(context, hotel);}}private static void PrintChangeTrackingInfo(DbContexts.DataAccess.BreakAwayContext context, DbContexts.Model.Lodging entity){var entry = context.Entry(entity);Console.WriteLine(entry.Entity.Name);Console.WriteLine("State: {0}", entry.State);Console.WriteLine("\nCurrent Values:");PrintPropertyValues(entry.CurrentValues);Console.WriteLine("\nOriginal Values:");PrintPropertyValues(entry.OriginalValues);Console.WriteLine("\nDatabase Values:");PrintPropertyValues(entry.GetDatabaseValues());}private static void PrintPropertyValues(DbPropertyValues values){foreach (var propertyName in values.PropertyNames){Console.WriteLine(" - {0}: {1}", propertyName, values[propertyName]);}}

方法分析:先从数据库取出一个实体,然后修改其Name属性,这个时候当前值(Current)和原始值(Original)都有了,分别是:修改后的值(还没提交,在内存中)和从库里取出来时实体的值。再使用Database.ExecuteSqlCommand执行了一段修改此对象在数据库中的值,这个时候数据库值(Database)也有了变化,这个实体的三个值都不相同了。还没看到打印结果,在执行entry.GetDatabaseValues()方法时报了一个EntitySqlException错:

找不到类型DbContexts.DataAccess.Lodging,项目的Lodging实体明明在DbContexts.Model.Lodging命名空间下,反复检查代码没发现任何问题,报这个错真是很疑惑。最后通过搜索引擎才知道这是EF4.1/4.2版本的一个bug,解决办法:修改实体和上下文到一个命名空间,或者使用EF4.3 release。看看本书作者Julie Lerman在msdn论坛上关于此bug的回复

换成4.3版本的EF问题就立马解决了(源码的libs目录下提供了EF4.3)。看下打印的结果:

结果分析:当前值为方法里修改的值、原始值是从数据库取出未做任何操作的值、数据库值是此时数据库里的值。当然新添加的实体不会有原始值和数据库值、删除的实体也不会有当前值,利用EntityState完善下方法:

        private static void PrintChangeTrackingInfo(DbContexts.DataAccess.BreakAwayContext context, DbContexts.Model.Lodging entity){var entry = context.Entry(entity);Console.WriteLine(entry.Entity.Name);Console.WriteLine("State: {0}", entry.State);if (entry.State != EntityState.Deleted)   //标记删除的实体不会有当前值
            {Console.WriteLine("\nCurrent Values:");PrintPropertyValues(entry.CurrentValues);}if (entry.State != EntityState.Added)   //新添加的时候不会有原始值和数据库值
            {Console.WriteLine("\nOriginal Values:");PrintPropertyValues(entry.OriginalValues);Console.WriteLine("\nDatabase Values:");PrintPropertyValues(entry.GetDatabaseValues());}}

为了测试重写下PrintLodgingInfo方法:

        /// <summary>/// 测试打印添加和删除时实体当前、原始和数据库值/// </summary>private static void PrintLodgingInfoAddAndDelete(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();PrintChangeTrackingInfo(context, hotel);   //默认var davesDump = (from d in context.Lodgingswhere d.Name == "Dave's Dump"select d).Single();context.Lodgings.Remove(davesDump);PrintChangeTrackingInfo(context, davesDump);   //测试删除实体var newMotel = new DbContexts.Model.Lodging { Name = "New Motel" };context.Lodgings.Add(newMotel);PrintChangeTrackingInfo(context, newMotel);  //测试新添加实体
            }}

当然上面打印实体类型的方法并不通用,修改第二个参数为object类型:

        /// <summary>/// 通用的打印实体方法/// </summary>private static void PrintChangeTrackingInfo(DbContexts.DataAccess.BreakAwayContext context, object entity){var entry = context.Entry(entity);Console.WriteLine("Type:{0}", entry.Entity.GetType());   //打印实体类型Console.WriteLine("State: {0}", entry.State);if (entry.State != EntityState.Deleted)   //标记删除的实体不会有当前值
            {Console.WriteLine("\nCurrent Values:");PrintPropertyValues(entry.CurrentValues);}if (entry.State != EntityState.Added)   //新添加的时候不会有原始值和数据库值
            {Console.WriteLine("\nOriginal Values:");PrintPropertyValues(entry.OriginalValues);Console.WriteLine("\nDatabase Values:");PrintPropertyValues(entry.GetDatabaseValues());}}

看看打印结果:

之前打印实体的各种属性都是通过遍历的形式(PrintPropertyValues方法)打印出来,如果仅取某个字段当然没必要这么麻烦,可以使用GetValue<TValue>:

        /// <summary>/// 打印实体单个属性/// </summary>private static void PrintOriginalName(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();hotel.Name = "Super Grand Hotel";string originalName = context.Entry(hotel).OriginalValues.GetValue<string>("Name");Console.WriteLine("Current Name: {0}", hotel.Name);  //Super Grand HotelConsole.WriteLine("Original Name: {0}", originalName);  //Grand Hotel}}

拷贝DbPropertyValues到实体:ToObject方法

        /// <summary>/// 拷贝DbPropertyValues到实体:ToObject方法/// </summary>private static void TestPrintDestination(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var reef = (from d in context.Destinationswhere d.Name == "Great Barrier Reef"select d).Single();reef.TravelWarnings = "Watch out for sharks!";Console.WriteLine("Current Values");PrintDestination(reef);Console.WriteLine("\nDatabase Values");DbPropertyValues dbValues = context.Entry(reef).GetDatabaseValues();PrintDestination((DbContexts.Model.Destination)dbValues.ToObject());  //ToObject方法创建Destination实例
            }}private static void PrintDestination(DbContexts.Model.Destination destination){Console.WriteLine("-- {0}, {1} --", destination.Name, destination.Country);Console.WriteLine(destination.Description);if (destination.TravelWarnings != null){Console.WriteLine("WARNINGS!: {0}", destination.TravelWarnings);}}

方法分析:从Destination表里取出Name为Great Barrier Reef的实体并修改其TravelWarnings字段,然后调用PrintDestination方法打印当前实体的各属性,再查出此实体在数据库里的值,并且通过ToObject方法把数据库取出来的这个对象也转换成了实体对象。这么转有什么好处呢?这个通过ToObject转换的Destination实例不会被数据库上下文追踪,所以对其做的任何改变都不会提交数据库。看看打印结果:

修改DbPropertyValues当前值:

调用上下文的Entry方法,传入要操作的实体对象,再打点就可以拿到实体的当前值(CurrentValues)、原始值(OriginalValues)、数据库值(GetDatabaseValues()),返回类型是DbPropertyValues,直接遍历就可以输出实体的所有属性。当然DbPropertyValues并不是只读的。写个方法修改试试:

        /// <summary>/// 修改DbPropertyValues当前值/// </summary>private static void ChangeCurrentValue(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();context.Entry(hotel).CurrentValues["Name"] = "Hotel Pretentious";Console.WriteLine("Property Value: {0}", hotel.Name);Console.WriteLine("State: {0}", context.Entry(hotel).State);  //Modified
            }}

类似于索引器的方式赋值即可,赋值后实体的状态已经是Modified了,显然已经被上下文追踪到了,这个时候调用上下文的SaveChanges方法将会提交到数据库。那么如果只是想打印和修改实体状态以供查看,并不像被提交到数据库怎么办?


最好的办法就是克隆,先克隆实体然后操作克隆之后的实体:

        /// <summary>/// 克隆实体:Clone/// </summary>private static void CloneCurrentValues(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();var values = context.Entry(hotel).CurrentValues.Clone();  //Clone方法values["Name"] = "Simple Hotel";Console.WriteLine("Property Value: {0}", hotel.Name);Console.WriteLine("State: {0}", context.Entry(hotel).State);  //Unchanged
            }}

设置实体的值:SetValues方法

当然实体的当前值、原始值和数据库值都是可以相互复制的:

        /// <summary>/// 设置实体的值:SetValues方法/// </summary>private static void UndoEdits(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var canyon = (from d in context.Destinationswhere d.Name == "Grand Canyon"select d).Single();canyon.Name = "Bigger & Better Canyon";var entry = context.Entry(canyon);entry.CurrentValues.SetValues(entry.OriginalValues);entry.State = EntityState.Unchanged;  //标记未修改
Console.WriteLine("Name: {0}", canyon.Name); //Grand Canyon}}

上面的方法演示了拷贝原始值到当前值,最终保存的是当前值。很方便,不需要挨个赋值。

再看看如何使用SetValues方法实现之前说的克隆实体:

        /// <summary>/// 克隆实体:SetValues/// </summary>private static void CreateDavesCampsite(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var davesDump = (from d in context.Lodgingswhere d.Name == "Dave's Dump"select d).Single();var clone = new DbContexts.Model.Lodging();context.Lodgings.Add(clone);context.Entry(clone).CurrentValues.SetValues(davesDump);  //克隆davesDump的值到新对象clone里clone.Name = "Dave's Camp";  //修改Name属性context.SaveChanges();  //最后提交修改
Console.WriteLine("Name: {0}", clone.Name);  //Dave's CampConsole.WriteLine("Miles: {0}", clone.MilesFromNearestAirport);  //32.65Console.WriteLine("Contact Id: {0}", clone.PrimaryContactId);  //1}}
exec sp_executesql N'insert [dbo].[Lodgings]([Name], [Owner], [MilesFromNearestAirport], [destination_id], [PrimaryContactId], [SecondaryContactId], [Entertainment], [Activities], [MaxPersonsPerRoom], [PrivateRoomsAvailable], [Discriminator])
values (@0, null, @1, @2, @3, null, null, null, null, null, @4)
select [LodgingId]
from [dbo].[Lodgings]
where @@ROWCOUNT > 0 and [LodgingId] = scope_identity()',N'@0 nvarchar(200),@1 decimal(18,2),@2 int,@3 int,@4 nvarchar(128)',@0=N'Dave''s Camp',@1=32.65,@2=1,@3=1,@4=N'Lodging'

很明显实体已经被克隆了。

获取和设置实体的单个属性:Property方法

        /// <summary>/// 获取和设置实体的单个属性:Property方法/// </summary>private static void WorkingWithPropertyMethod(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var davesDump = (from d in context.Lodgingswhere d.Name == "Dave's Dump"select d).Single();var entry = context.Entry(davesDump);entry.Property(d => d.Name).CurrentValue = "Dave's Bargain Bungalows";  //设置Name属性
Console.WriteLine("Current Value: {0}", entry.Property(d => d.Name).CurrentValue);  //Dave's Bargain BungalowsConsole.WriteLine("Original Value: {0}", entry.Property(d => d.Name).OriginalValue);  //Dave's DumpConsole.WriteLine("Modified?: {0}", entry.Property(d => d.Name).IsModified);   //True
            }}

同样可以查询出实体的哪些属性被修改了:IsModified方法

        /// <summary>/// 查询实体被修改字段:IsModified方法/// </summary>private static void FindModifiedProperties(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var canyon = (from d in context.Destinationswhere d.Name == "Grand Canyon"select d).Single();canyon.Name = "Super-Size Canyon";canyon.TravelWarnings = "Bigger than your brain can handle!!!";var entry = context.Entry(canyon);var propertyNames = entry.CurrentValues.PropertyNames;  //获取所有的Name列
IEnumerable<string> modifiedProperties = from name in propertyNameswhere entry.Property(name).IsModifiedselect name;foreach (var propertyName in modifiedProperties){Console.WriteLine(propertyName);  //Name、TravelWarnings
                }}}

前面的章节已经讲解了如何查询一对一、一对多等关系的导航属性了,还不了解的点这里。现在讲讲如何修改导航属性:

        /// <summary>/// 修改导航属性(Reference):CurrentValue方法/// </summary>private static void WorkingWithReferenceMethod(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var davesDump = (from d in context.Lodgingswhere d.Name == "Dave's Dump"select d).Single();var entry = context.Entry(davesDump);entry.Reference(l => l.Destination).Load();   //显示加载var canyon = davesDump.Destination;Console.WriteLine("Current Value After Load: {0}", entry.Reference(d => d.Destination).CurrentValue.Name);var reef = (from d in context.Destinationswhere d.Name == "Great Barrier Reef"select d).Single();entry.Reference(d => d.Destination).CurrentValue = reef;   //修改Console.WriteLine("Current Value After Change: {0}", davesDump.Destination.Name);}}

打印结果:
Current Value After Load: Grand Canyon
Current Value After Change: Great Barrier Reef

注:上面的方法并没有调用上下文的SaveChanges方法,故程序跑完数据也不会保存到数据库,本文所有方法仅作演示都未提交数据库。

有Reference找单个属性的,那么自然也有Collection找集合属性的:

        /// <summary>/// 修改导航属性(Collection):CurrentValue方法/// </summary>private static void WorkingWithCollectionMethod(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var res = (from r in context.Reservationswhere r.Trip.Description == "Trip from the database"select r).Single();var entry = context.Entry(res);entry.Collection(r => r.Payments).Load();Console.WriteLine("Payments Before Add: {0}", entry.Collection(r => r.Payments).CurrentValue.Count);var payment = new DbContexts.Model.Payment { Amount = 245 };context.Payments.Add(payment);entry.Collection(r => r.Payments).CurrentValue.Add(payment);  //修改Console.WriteLine("Payments After Add: {0}", entry.Collection(r => r.Payments).CurrentValue.Count);}}

打印结果:
Payments Before Add: 1
Payments After Add: 2

从数据库取出实体加载到内存中,可能并不立马就展示给用户看。在进行一系列的排序、筛选等操作再展示出来。但是怎么确定展示的时候这些实体没有被修改过呢?可以使用Reload方法重新加载:

        /// <summary>/// 取当前最新的数据库值:Reload方法/// </summary>private static void ReloadLodging(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var hotel = (from d in context.Lodgingswhere d.Name == "Grand Hotel"select d).Single();  //取出实体context.Database.ExecuteSqlCommand(@"UPDATE dbo.Lodgings SET Name = 'Le Grand Hotel' WHERE Name = 'Grand Hotel'");   //立马修改实体值(这个时候数据库中的值已改变,但是取出来放在内存中的值并没改变)Console.WriteLine("Name Before Reload: {0}", hotel.Name);Console.WriteLine("State Before Reload: {0}", context.Entry(hotel).State);context.Entry(hotel).Reload();Console.WriteLine("Name After Reload: {0}", hotel.Name);Console.WriteLine("State After Reload: {0}", context.Entry(hotel).State);}}

打印结果:
Name Before Reload: Grand Hotel
State Before Reload: Unchanged
Name After Reload: Le Grand Hotel
State After Reload: Unchanged

可以看出Reload方法已经重新取出了数据库中的最新值。来看看Reload方法生成的sql:

SELECT 
[Extent1].[Discriminator] AS [Discriminator], 
[Extent1].[LodgingId] AS [LodgingId], 
[Extent1].[Name] AS [Name], 
[Extent1].[Owner] AS [Owner], 
[Extent1].[MilesFromNearestAirport] AS [MilesFromNearestAirport], 
[Extent1].[destination_id] AS [destination_id], 
[Extent1].[PrimaryContactId] AS [PrimaryContactId], 
[Extent1].[SecondaryContactId] AS [SecondaryContactId], 
[Extent1].[Entertainment] AS [Entertainment], 
[Extent1].[Activities] AS [Activities], 
[Extent1].[MaxPersonsPerRoom] AS [MaxPersonsPerRoom], 
[Extent1].[PrivateRoomsAvailable] AS [PrivateRoomsAvailable]
FROM [dbo].[Lodgings] AS [Extent1]
WHERE ([Extent1].[Discriminator] IN ('Resort','Hostel','Lodging')) AND ([Extent1].[LodgingId] = 1)

当然Reload方法也会保存内存中修改的数据,这个并不会冲突。在方法里的linq查询后面加上:hotel.Name = "A New Name"; 打印结果就是这样的了:
Name Before Reload: A New Name
State Before Reload: Modified
Name After Reload: Le Grand Hotel
State After Reload: Unchanged

注意,代码里修改的Name已经显示了,并且标记实体状态为Modified了,Modified会在调用上下文的SaveChanges方法的时候提交到数据库。这个过程是这样的:

加载实体到内存中 - 在内存中对实体的某个属性进行修改 - 使用ExecuteSqlCommand方法执行sql修改数据库里该实体的值 - 调用Reload取出数据库里本实体的最新值 - 调用SaveChanges方法的话,在内存中对实体的修改也会被提交到数据库

之前操作了单个实体,现在看看如何读取关联实体和状态。使用DbContext.ChangeTracker.Entries方法:

     /// <summary>/// 读取相关联的实体和状态:DbContext.ChangeTracker.Entries方法/// </summary>private static void PrintChangeTrackerEntries(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var res = (from r in context.Reservationswhere r.Trip.Description == "Trip from the database"select r).Single();context.Entry(res).Collection(r => r.Payments).Load();
res.Payments.Add(
new DbContexts.Model.Payment { Amount = 245 });var entries = context.ChangeTracker.Entries();foreach (var entry in entries){Console.WriteLine("Entity Type: {0}", entry.Entity.GetType());Console.WriteLine(" - State: {0}", entry.State);}}}

添加了一个从表实体,并读取所有关联实体和其状态,打印结果:
Entity Type: DbContexts.Model.Payment - State: Added
Entity Type: DbContexts.Model.Reservation - State: Unchanged
Entity Type: DbContexts.Model.Payment - State: Unchanged

EF里如何解决更新数据时的冲突

正常根据实体的主键修改实体的时候,EF是不会判断数据修改之前有没有被别的人修改过,但是如果做了并发控制,EF在更新某条记录的时候才会抛错。这个系列文章的demo里有两个实体做了并发控制:Person类的SocialSecurityNumber字段被标记了ConcurrencyCheck;Trip类的RowVersion字段被标记了Timestamp。来写一个触发DbUpdateConcurrencyException异常的方法并处理这个异常:

        /// <summary>/// 修改实体/// </summary>private static void ConcurrencyDemo(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var trip = (from t in context.Trip.Include(t => t.Destination)where t.Description == "Trip from the database"select t).Single();trip.Description = "Getaway in Vermont";context.Database.ExecuteSqlCommand(@"UPDATE dbo.Trips SET CostUSD = 400 WHERE Description = 'Trip from the database'");SaveWithConcurrencyResolution(context);}}/// <summary>/// 尝试保存/// </summary>private static void SaveWithConcurrencyResolution(DbContexts.DataAccess.BreakAwayContext context){try{context.SaveChanges();}catch (DbUpdateConcurrencyException ex){ResolveConcurrencyConflicts(ex);SaveWithConcurrencyResolution(context);}}

方法分析:取出实体 - 修改实体Description属性(此时实体状态为Modified)- 使用ExecuteSqlCommand执行sql修改了CostUSD和Description字段(修改后时间戳已经不同了,PS:使用ExecuteSqlCommand执行sql不需要调用SaveChanges方法)- 调用上下文的SaveChanges方法保存之前被标记为Modified的实体,这个时候就会报一个DbUpdateConcurrencyException的异常,因为时间戳列已经找不到了,这个更新的where条件根本找不到记录了。有时间戳的列更新都是双条件,时间戳详细用法点这里了解。

尝试写个方法解决这个冲突:

        /// <summary>/// 解决冲突/// </summary>private static void ResolveConcurrencyConflicts(DbUpdateConcurrencyException ex){foreach (var entry in ex.Entries){Console.WriteLine("Concurrency conflict found for {0}", entry.Entity.GetType());Console.WriteLine("\nYou are trying to save the following values:");PrintPropertyValues(entry.CurrentValues);  //用户修改的值
Console.WriteLine("\nThe values before you started editing were:");PrintPropertyValues(entry.OriginalValues);  //从库里取出来时的值var databaseValues = entry.GetDatabaseValues();  //即时数据库的值Console.WriteLine("\nAnother user has saved the following values:");PrintPropertyValues(databaseValues);Console.WriteLine("[S]ave your values, [D]iscard you changes or [M]erge?");var action = Console.ReadKey().KeyChar.ToString().ToUpper(); //读取用户输入的字母switch (action){case "S":entry.OriginalValues.SetValues(databaseValues);  //拷贝数据库值到当前值(恢复时间戳)break;case "D":entry.Reload();  //重新加载break;case "M":var mergedValues = MergeValues(entry.OriginalValues, entry.CurrentValues, databaseValues);//合并entry.OriginalValues.SetValues(databaseValues);  //拷贝数据库值到当前值(恢复时间戳)entry.CurrentValues.SetValues(mergedValues);     //拷贝合并后的值到当前值,最终保存的是当前值break;default:throw new ArgumentException("Invalid option");}}}

捕获到异常后告知用户要修改实体的原始值(用户修改前从数据库取出来的值)、现在的值(用户修改的值)、数据库里的值(此时数据库里的值,这个值已被修改,不是用户修改前取出来的值了),打印出来的结果显示已经有人修改了这条记录了。最后是问用户是否保存修改。分别是保存、放弃、合并修改。

用户输入"S"表示“保存”,case语句块里执行的操作是拷贝数据库值到原始值,这里该有疑惑了,调用SaveChanges方法保存的也是currentValues当前值,跟databaseValues数据库值还有OriginalValues原始值没有任何关系啊。其实这么操作是恢复一下时间戳的值方便更新,之前说过timestamp的列更新条件是两个,任何一个不对都更新不了。看看sql:

exec sp_executesql N'update [dbo].[Trips]
set [Description] = @0, [CostUSD] = @1
where (([Identifier] = @2) and ([RowVersion] = @3))
select [RowVersion]
from [dbo].[Trips]
where @@ROWCOUNT > 0 and [Identifier] = @2',N'@0 nvarchar(max) ,@1 decimal(18,2),@2 uniqueidentifier,@3 binary(8)',@0=N'Getaway in Vermont',@1=1000.00,@2='CF2E6BD3-7393-440C-941A-
9124C61CE04A',@3=0x00000000000007D2

结果只保存了自己的修改:

用户输入“D”表示“放弃”,case语句块里执行的是Reload方法,这个方法之前已经介绍过了,是重新加载数据库里的最新值(Latest Value)。恢复下数据库数据再执行下方法,看看sql:

SELECT 
[Extent1].[Identifier] AS [Identifier], 
[Extent1].[StartDate] AS [StartDate], 
[Extent1].[EndDate] AS [EndDate], 
[Extent1].[Description] AS [Description], 
[Extent1].[CostUSD] AS [CostUSD], 
[Extent1].[RowVersion] AS [RowVersion], 
[Extent1].[DestinationId] AS [DestinationId]
FROM [dbo].[Trips] AS [Extent1]
WHERE [Extent1].[Identifier] = cast('cf2e6bd3-7393-440c-941a-9124c61ce04a' as uniqueidentifier)

取了下数据库里该实体最新的值(使用ExecuteSqlCommand更新后的值),没有其他任何更新语句,就是放弃本次修改的意思,但是之前ExecuteSqlCommand方法执行的修改是有效的,看看结果:

上面的“保存修改”和“放弃修改”只能保存一个,如果让用户修改的和ExecuteSqlCommand的修改同时生效呢,选择M,意为合并。看看合并方法:

        /// <summary>/// 合并/// </summary>private static DbPropertyValues MergeValues(DbPropertyValues original, DbPropertyValues current, DbPropertyValues database){var result = original.Clone();  //拷贝原始值并存放合并后的值foreach (var propertyName in original.PropertyNames)  //遍历原始值的所有列
            {if (original[propertyName] is DbPropertyValues)  //判断当前列是否复杂类型(很少)
                {var mergedComplexValues =MergeValues((DbPropertyValues)original[propertyName],(DbPropertyValues)current[propertyName],(DbPropertyValues)database[propertyName]);   //是复杂类型的话就使用递归合并复杂类型的值((DbPropertyValues)result[propertyName]).SetValues(mergedComplexValues);}else  //是普通里的话就和当前值、数据库值、原始值各种对比。修改了就赋值{if (!object.Equals(current[propertyName], original[propertyName]))result[propertyName] = current[propertyName];else if (!object.Equals(database[propertyName], original[propertyName]))result[propertyName] = database[propertyName];}}return result;}

看看sql:

exec sp_executesql N'update [dbo].[Trips]
set [Description] = @0, [CostUSD] = @1
where (([Identifier] = @2) and ([RowVersion] = @3))
select [RowVersion]
from [dbo].[Trips]
where @@ROWCOUNT > 0 and [Identifier] = @2',N'@0 nvarchar(max) ,@1 decimal(18,2),@2 uniqueidentifier,@3 binary(8)',@0=N'Getaway in Vermont',@1=400.00,@2='CF2E6BD3-7393-440C-941A-9124C61CE04A',@3=0x00000000000007DC

看看结果:

用户修改和ExecuteSqlCommand修改的都保存上了。

最后讲一个更实用的东西:重写上下文的SaveChanges方法记录结果集里实体的各种增/删/改。
先到BreakAwayContext类里添加一个属性标识使用数据库上下文的SaveChanges方法还是使用自定义的SaveChanges方法:public bool LogChangesDuringSave { get; set; }

来看一个方法:

        /// <summary>/// 记录结果集的各种:增 / 删 /改/// </summary>private static void TestSaveLogging(){using (var context = new DbContexts.DataAccess.BreakAwayContext()){var canyon = (from d in context.Destinationswhere d.Name == "Grand Canyon"select d).Single();//加载主表数据
context.Entry(canyon).Collection(d => d.Lodgings).Load();//显示加载出从表相关数据canyon.TravelWarnings = "Take a hat!";//修改主表字段context.Lodgings.Remove(canyon.Lodgings.First());//删除相关联从表的第一条数据context.Destinations.Add(new DbContexts.Model.Destination { Name = "Seattle, WA" });//添加一条主表数据context.LogChangesDuringSave = true;  //设置标识,使用自定义的SaveChanges方法
                context.SaveChanges();}}

增加、修改、删除操作等都有。运行这个方法前需要在BreakAwayContext类里添加记录的帮助类方法:

        /// <summary>/// 记录帮助类方法/// </summary>private void PrintPropertyValues(DbPropertyValues values, IEnumerable<string> propertiesToPrint, int indent = 1){foreach (var propertyName in propertiesToPrint){var value = values[propertyName];if (value is DbPropertyValues){Console.WriteLine("{0}- Complex Property: {1}", string.Empty.PadLeft(indent), propertyName);var complexPropertyValues = (DbPropertyValues)value;PrintPropertyValues(complexPropertyValues, complexPropertyValues.PropertyNames, indent + 1);}else{Console.WriteLine("{0}- {1}: {2}", string.Empty.PadLeft(indent), propertyName, values[propertyName]);}}}private IEnumerable<string> GetKeyPropertyNames(object entity){var objectContext = ((IObjectContextAdapter)this).ObjectContext;return objectContext.ObjectStateManager.GetObjectStateEntry(entity).EntityKey.EntityKeyValues.Select(k => k.Key);}

再在BreakAwayContext类里重写下上下文的SaveChanges方法:

        /// <summary>/// 重写SaveChanges方法/// </summary>public override int SaveChanges(){if (LogChangesDuringSave)  //根据表示判断用重写的SaveChanges方法,还是普通的上下文SaveChanges方法
            {var entries = from e in this.ChangeTracker.Entries()where e.State != EntityState.Unchangedselect e;   //过滤所有修改了的实体,包括:增加 / 修改 / 删除foreach (var entry in entries){switch (entry.State){case EntityState.Added:Console.WriteLine("Adding a {0}", entry.Entity.GetType());PrintPropertyValues(entry.CurrentValues, entry.CurrentValues.PropertyNames);break;case EntityState.Deleted:Console.WriteLine("Deleting a {0}", entry.Entity.GetType());PrintPropertyValues(entry.OriginalValues, GetKeyPropertyNames(entry.Entity));break;case EntityState.Modified:Console.WriteLine("Modifying a {0}", entry.Entity.GetType());var modifiedPropertyNames = from n in entry.CurrentValues.PropertyNameswhere entry.Property(n).IsModifiedselect n;PrintPropertyValues(entry.CurrentValues, GetKeyPropertyNames(entry.Entity).Concat(modifiedPropertyNames));break;}}}return base.SaveChanges();  //返回普通的上下文SaveChanges方法}

运行结果为:

所有添加/修改/删除都记录下来了,这个可以方便我们在写程序的时候做更细微的控制,毕竟EF对实体操作的依据就是实体的各种状态。

本文源码

EF DbContext 系列文章导航:
  1. EF如何操作内存中的数据和加载外键数据:延迟加载、贪婪加载、显示加载  本章源码
  2. EF里单个实体的增查改删以及主从表关联数据的各种增删改查  本章源码
  3. 使用EF自带的EntityState枚举和自定义枚举实现单个和多个实体的增删改查  本章源码
  4. EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态  本章源码
  5. EF里如何定制实体的验证规则和实现IObjectWithState接口进行验证以及多个实体的同时验证  本章源码
  6. 重写ValidateEntity虚方法实现可控的上下文验证和自定义验证  本章源码
posted on 2014-02-25 19:42 NET未来之路 阅读(...) 评论(...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/3567534.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/399701.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux命令与shell

为什么80%的码农都做不了架构师&#xff1f;>>> 资料来自&#xff1a;《http://blog.chinaunix.net/uid-14880649-id-2954340.html》 所谓shell就是命令解释程序。它提供了程序设计接口&#xff0c;可以使用程序来编程。学习shell对于Linux初学者理解Linux系统是非…

ReportViewer不连接数据库,自定义DataSet导出到报表

先建一个窗体&#xff0c;添加reportview,然后添加RDLC文件&#xff0c;然后添加数据集 1、添加一个数据集 点确定后界面如下 在空白处右键 修改名称 添加行 重命名行 表效果 2、添加报表 确定后出现下面界面 然后添加资料数据源 点击新增&#xff0c;选择资料集&#xff0c;出…

[转] 前端学习必备基础(1)

我们生活在五彩缤纷的世界里&#xff0c;天空、草地、海洋、漫无边际的薰衣草都有它们各自的色彩。你、我、他也有自己的色彩&#xff0c;代表个人特色的衣着、家装、装饰物的色彩&#xff0c;可以充分反映人的性格、爱好、品位。 设计爱好者对色彩的喜爱更是“如痴如狂”&…

最全的jquery datatables api 使用详解

https://www.cnblogs.com/amoniyibeizi/p/4548111.html 最全的jquery datatables api 使用详解 学习可参考&#xff1a;http://www.guoxk.com/node/jquery-datatables http://yuemeiqing2008-163-com.iteye.com/blog/2006942 分别导入css和js文件 <link href"~/Conte…

haut-1280 诡异的迷宫

1280: 诡异的迷宫 时间限制: 2 秒 内存限制: 128 MB提交: 174 解决: 27提交 状态 题目描述 Simple最近刷题&#xff08;打游戏&#xff09;刷多了&#xff0c;一觉醒来发现自己到了一个迷宫里&#xff0c;怎么也出不去了。这时传来了一句话&#xff0c;告诉Simple必须按顺序收…

一个经典实例理解继承与多态原理与优点(附源码)---面向对象继承和多态性理解得不够深刻的同学请进...

一 引子 都说面向对象的4大支柱是抽象&#xff0c;封装&#xff0c;继承与多态。但是一些初涉编程的开发人员&#xff0c;体会不到继承与多态的妙用&#xff0c;本文就试以一个经典实例来诠释继承与多态的用武之地。本实例的需求来自《重构》一书。 二 需求 1. 任务说明 我们的…

推荐几款好用的云笔记软件

一直钟爱印象笔记&#xff0c;程序员的电脑上必装的软件&#xff0c;但最近期限到了&#xff0c;再也不能像以前无限制的上传文件&#xff0c;续费也比去年的只要九块九一年高出了很多倍&#xff0c;因此&#xff0c;注册试用了其他的笔记&#xff0c;发现云笔记众多&#xff0…

Sublime Text 3 初试牛刀

每次我在其他视频网站上看学习视频的时候&#xff0c;看着老师用的编辑器高大上档次&#xff0c;而我一般用Notepad&#xff0c;和Dreamweaver去编辑网页&#xff0c;需要每一行代码&#xff0c;打进去&#xff0c;效率低。最近看到sublime编辑器&#xff0c;在网上搜了一下说是…

[C++学习历程]基础部分 C++中的函数学习

本文地址&#xff1a;http://blog.csdn.net/sushengmiyan/article/details/20305815 作者&#xff1a;sushengmiyan 一。静态变量&#xff1a; 局部变量是线程到达定义的地方的时候进行初始化&#xff0c;如果定义在函数中&#xff0c;那么每次函数调用的时候&#xff0c;都会进…

linux下安装Mysql(干货!!!)解决mysql 1130问题,远程登录问题

转载自&#xff1a;http://www.cnblogs.com/xxoome/p/5864912.html linux版本&#xff1a;CentOS7 64位 1、下载安装包“mysql-5.6.33-linux-glibc2.5-x86_64.tar.gz” # 安装依赖 yum -y install perl perl-devel autoconf libaio 2、把下载的安装包移动到/usr/local/下。…

Openstack Neutron : 安全

目录 - iptable&#xff1a;起源 - tables - chains - rules - 方向 - Security group 安全组&#xff1a; - Firewall 防火墙&#xff1a; - 更高的安全 - 无处安放的安全 - 公共安全 当业务从传统环境迁移到云上之后&a…

SQL语句汇总(三)——聚合函数、分组、子查询及组合查询

https://www.cnblogs.com/ghost-xyx/p/3811036.html SQL语句汇总&#xff08;三&#xff09;——聚合函数、分组、子查询及组合查询 拖了一个星期&#xff0c;终于开始写第三篇了。走起&#xff01; 聚合函数&#xff1a; SQL中提供的聚合函数可以用来统计、求和、求最值等等…

iOS应用国际化教程(2014版)

本文转载至 http://www.cocoachina.com/industry/20140526/8554.html 这篇教程将通过一款名为iLikeIt的应用带你了解最基础的国际化概念&#xff0c;并为你的应用添加国际化的支持。该示例应用有一个标签和一个You Like&#xff1f;按钮&#xff0c;用户无论何时点击You Like?…

公众平台商户接入(微信支付)功能申请教程

场景及类型介绍 商家可以申请公众账号支付和APP&#xff08;应用客户端&#xff09;支付两种接入微信支付方式。 公众账号支付&#xff1a;用户在微信公众帐号内使用微信支付消费&#xff0c;案例&#xff1a;易迅、QQ充值。 APP&#xff08;应用客户端&#xff09;支付&#x…

wxPython python3.x下载地址

2019独角兽企业重金招聘Python工程师标准>>> wxPython python3.x下载地址 http://wxpython.org/Phoenix/snapshot-builds/ 转载于:https://my.oschina.net/laugh2last/blog/504688

爬山算法和模拟退火算法简介(转)

源&#xff1a;爬山算法和模拟退火算法简介 一. 爬山算法 ( Hill Climbing ) 介绍模拟退火前&#xff0c;先介绍爬山算法。爬山算法是一种简单的贪心搜索算法&#xff0c;该算法每次从当前解的临近解空间中选择一个最优解作为当前解&#xff0c;直到达到一个局部最优解。 爬山算…

How to connect oracle databse

1. 下載客戶端Oracle Developer Tools for Visual Studio_32bit 安裝后通過配置tnsnames.ora指定連接 C:\app\user name\product\11.2.0\client_1\Network\Admin\SERVER(DESCRIPTION(ADDRESS(PROTOCOLTCP)(HOSTIP Address)(PORT1521))(CONNECT_DATA(SIDSID))) 重啓后可以在VS20…

wordpress实现搜索页关键词高亮

http://www.hehaibao.com/wordpress-search-word-highlight/ 今天给网站搜索页加了关键词高亮功能&#xff0c;分享出来&#xff0c;希望对小伙伴们有所帮助。 实现效果如下图&#xff1a; 那么我们直接先上主要代码&#xff1a; 1 2 3 4 5 6 7 8 9 10 11<?php$s trim(ge…

数组和指针的区别

1、访问方式不同 ⑴数组的下标引用&#xff08;读一次内存&#xff09; char ary[5] "hello",c; ...... c ary[2]; ⑵对指针的引用&#xff08;读两次内存&#xff09; char *P,c; ...... c *p;   ⑶对指针进行下标引用&#xff08;读两次内存&#xff09; char…

SQL图像查看器 —— SQL Image Viewer

有时候往数据库里面存储了一些图片&#xff0c;但是如果不写读取程序的话&#xff0c;就不知道存储的对不对。 或者查看SQL数据库里面二进制看不懂&#xff0c;这个看图片很直观的。 就需要SQL Image Viewer这么一个?B的软件了 自己去找下载地址吧&#xff0c;我没有破解版。 …