本文目錄html
文章開始前建議你們爲了更好的記憶最好本身實現文中的全部方法。若是非要直接運行個人demo,必要的時候須要恢復下數據庫數據,不然找不到記錄。sql
以前的章節已經演示了context.Entry方法能夠拿到實體的狀態(EntityState),咱們看一個方法:數據庫
/// <summary> /// 單個實體的狀態 /// </summary> private static void PrintState() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var canyon = (from d in context.Destinations where 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還能夠操做實體的當前值、原始值和數據庫值。他們分別是:併發
來看一個例子:ide
/// <summary> /// 打印實體當前、原始和數據庫值 /// </summary> private static void PrintLodgingInfo() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var hotel = (from d in context.Lodgings where 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 版本的一個bug,解決辦法:修改實體和上下文到一個命名空間,或者使用EF4.3 release。看看本書做者 Julie Lerman 在msdn論壇上關於此bug的 回覆ui
換成4.3版本的EF問題就立馬解決了(源碼的libs目錄下提供了EF4.3)。看下打印的結果:this
結果分析:當前值爲方法裏修改的值、原始值是從數據庫取出未作任何操做的值、數據庫值是此時數據庫裏的值。固然新添加的實體不會有原始值和數據庫值、刪除的實體也不會有當前值,咱們利用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方法:spa
/// <summary> /// 測試打印添加和刪除時實體當前、原始和數據庫值 /// </summary> private static void PrintLodgingInfoAddAndDelete() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var hotel = (from d in context.Lodgings where d.Name == "Grand Hotel" select d).Single(); PrintChangeTrackingInfo(context, hotel); //默認 var davesDump = (from d in context.Lodgings where 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.Lodgings where 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 Hotel Console.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.Destinations where 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.Lodgings where 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.Lodgings where 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.Destinations where 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.Lodgings where 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 Camp Console.WriteLine("Miles: {0}", clone.MilesFromNearestAirport); //32.65 Console.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.Lodgings where 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 Bungalows Console.WriteLine("Original Value: {0}", entry.Property(d => d.Name).OriginalValue); //Dave's Dump Console.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.Destinations where 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 propertyNames where entry.Property(name).IsModified select name; foreach (var propertyName in modifiedProperties) { Console.WriteLine(propertyName); //Name、TravelWarnings } } }
前面的章節已經講解了如何查詢一對1、一對多等關係的導航屬性了,還不瞭解的點 這裏 。如今講講如何修改導航屬性:
/// <summary> /// 修改導航屬性(Reference):CurrentValue方法 /// </summary> private static void WorkingWithReferenceMethod() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var davesDump = (from d in context.Lodgings where 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.Destinations where 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.Reservations where 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.Lodgings where 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.Reservations where 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.Destinations where 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.Unchanged select 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.PropertyNames where entry.Property(n).IsModified select n; PrintPropertyValues(entry.CurrentValues, GetKeyPropertyNames(entry.Entity).Concat(modifiedPropertyNames)); break; } } } return base.SaveChanges(); //返回普通的上下文SaveChanges方法 }
運行結果爲:
全部添加/修改/刪除都記錄下來了,這個能夠方便咱們作更細微的控制,畢竟EF對實體操做的依據就是實體的各類狀態。