前言
對於應用程序開發者來說,通常不需要考慮太多對於Entity Framework中的變更追蹤(change tracking),但是變更追蹤和DetectChanges方法是棧的一部分,在這其中,易用性和性能就緊密關聯。所以,鑒於此理由,對於你繼續看接下來的內容那將是非常有幫助,轉載地址:《https://blog.oneunicorn.com/2012/03/10/secrets-of-detectchanges-part-1-what-does-detectchanges-do》。
DetectChanges用途
接下來的內容將用下兩個簡單的Code First Model和Context(一個是Post類,一個是Blog類,一個博客可以發表多篇文章,但一篇文章只屬於一個博客)
public class Blog { public int Id { get; set; } public string Title { get; set; } public virtual ICollection Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public virtual Blog Blog { get; set; } }
EF上下文類:
public class AnotherBlogContext : DbContext { public AnotherBlogContext() : base("name=DBConnectionString") { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
變更追蹤問題
大部分EF應用程序都利用快照式變更追蹤,這就意味着在實體中我們不需要寫任何代碼來保持追蹤或者說通知更改的上下文。
下面我們用代碼來演示這點
using (var context = new AnotherBlogContext()) var post = context.Posts .Single(p => p.Title == "My First Post"); post.Title = "My Best Post"; context.SaveChanges(); }
上述代碼中,我們查出一個滿足條件的發表實體,同時修改其標題,最終改變被保存並返回到數據庫中。
但是標題屬性並沒有發生什么特殊的改變,因為它僅僅是C#類中的一個自動屬性而已,根據追蹤的事實是,它確確實實發生了改變(類似於一個臟數據),或者說真實的事實是原始的屬性值是 My First Post ,在實體中也沒有什么去通知上下文已經發生了什么。
那么問題來了,SaveChanges怎么確定它需要作出行為來更改數據庫中的標題呢?
答案就是使用快照式變更追蹤和DetectChanges方法
快照式變更追蹤和DetectChanges
當從數據庫進行查詢時,EF上下文便捕獲了每個實體屬性的快照,因此在上述例子中,當查詢時,EF上下文中在快照中便記錄了Post實體中的標題屬性值My First Post。當SaveChanges時,將會自動調用DetectChanges方法,此方法將掃描上下文中所有實體,並比較當前屬性值和存儲在快照中的原始屬性值。如果被找到的屬性值發生了改變,此時EF將會與數據庫進行交互,進行數據更新。在上述例子中,當前值My Best Post被檢測到與原始值不相同,所以必然會進行相應的更新。
DetectChanges其他用途
事實上,DetectChanges用途遠不止於此,為什么這樣說呢?待我詳細道來。此方法的大部分用途基本上是基於修改的范疇,那么到底修改什么呢?簡而言之就是:更改實體之間的引用以及內部的狀態和索引。似乎有點難懂,我們就實例來進行理解。
在上述的前提下,我們繼續修改發表(Post)實體的外鍵,代碼如下:
using (var context = new AnotherBlogContext()) { context.Blogs.Load();
var post = context.Posts .Single(p => p.Title == "My First Post"); post.Title = "My Best Post";
post.BlogId = 7;
context.SaveChanges(); }
當在外鍵上做處修改時,當SaveChanges時會回調DetectChanges方法,此時該方法將作出以下行為:
(1)確保新添加的外鍵FK被保存到數據庫中,就像任何屬性值發生了改變一樣。
(2)它將檢測博客(Blog)實體中的主鍵是否匹配新添加的BogId(FK),同時上下文將會追蹤新添加的BogId,如果匹配通過,將更改Blog導航屬性來指向這個實體
(3)因為Blog實體對象里有一個相對應Posts的導航屬性,此方法也就確保這些Posts會被更新。也就是說,這個查詢出的Post將會從原有的Blog實體中的Post集合中被刪除,並添加到Blog實體中的Posts集合中與現在的關聯。
(4)更新Blog實體和所有屬性的狀態以及與此實體和屬性相關聯的狀態。
(5)更新內部的索引。
【注意】如果不是外鍵而是導航屬性發生了改變的話,也會同樣如上作出相應的更新。
考慮性能
我們假設在上下文中跟蹤成千個實體並且一旦實體發生改變,就得作出相應的行為,這樣頻繁的回調DetectChanges方法占用的資源和消耗的性能可想而知代價之大。即使你的應用程序沒有成千個實體,這樣一來將不會產生性能瓶頸,但是如果你嘗試去優化DetectChanges,這樣你就不需要擔心在代碼中出現一些你意想不到的細小錯誤,所以毋庸置疑優化將是一種不錯的策略。請繼續往下看!
自動調用DetectChanges的時機上下文什么時候知道?
當調用SaveChanges時,這一刻是上下文需要知道實體是否已經發生改變的最重要的一刻,那是很顯然的。如果發生的改變是未知的,那么SaveChanges將無法確定向數據庫是做出是增加、修改還是刪除,這也就是為什么DetectChanges會被SaveChanges回調的原因。(當然,除非這種回調通過明確顯式的禁用)即使你在EF4中使用ObjectContext。
然而上下文也需要知道在其他時刻的改變,例如,如果你要求上下文獲得一個實體的狀態,所以為了知道該實體是未發生改變(UnChanged)還是修改了(Modified),然后上下文就需要知道這個實體中任何屬性的值是否已經發生了改變。
請看以下代碼:
using (var context = new AnotherBlogContext()) { var post = context.Posts.Single(p => p.Title == "My First Post"); post.Title = "My Best Post"; Console.WriteLine(context.Entry(post).State); //或者 DbEntityEntry entry = context.Entry<Post>(post); Console.WriteLine(entry.State); }
Post實體已經發生了改變,因此將有理有據的輸出如下:
在上述中當調用SaveChanges時會回調DetectChanges,其實DetectChanges方法中的所謂的修改發生在各個時期,請看下面代碼,在上下文中查找實體:
using (var context = new AnotherBlogContext()) { var post = context.Posts.First(p => p.BlogId == 1); post.BlogId = 2; var blog2 = context.Blogs.Find(2); Assert.Same(blog2, post.Blog); Assert.Contains(post, blog2.Posts); }
上述代碼一個帶有外鍵為(BlogId)為1的Post實體,然后改變其外鍵值為2,接下來我們要找到Blog中主鍵為2的實體,當Post實體被帶入到上下文中時,EF實體對其進行修改。這似乎是可能的,如果EF現在知道Post實體的外鍵為2,並且回調了DetectChanges方法的話,這種情況將是可以發生的。結果進行驗證,斷言通過,說明Find方法回調了DetectChanges方法。
會回調DetectChanges方法的方法
從以上顯而易見,DetectChanges方法通常需要通過DbContext中的方法被回調同時其關聯類也如預期一樣相應的進行回調。這就是為什么DetectChanges方法會被如下方法回調:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContex.Entry
DbChangeTracker.Entries
特別注意SaveChanges和ValidateEntity
DetectChanges被調用是SaveChanges實現的一部分,這就意味着,如果你在上下文中重寫了SaveChanges的話,那么在你的SaveChanges被調用之前DetectChanges不會被回調的。這讓人很惱火,特別是當檢測一個實體是被修改還是未被修改的時候,因為它的狀態可能直到DetectChanges被回調的時候才被修改,可喜的是,像這種情況用DbContext中的SaveChanges比過去用ObjectContext中的SaveChanges發生的概率要低很多,因為Entry和Entry方法被設計用來訪問實體中的狀態並自動調用DetectChanges。
ValidateEntity是在GetValidationErrors或者SaveChanges期間的自定義驗證,與SaveChanges不同的是,它會在DetectChanges方法調用之后再調用,道理很簡單,驗證肯定是在保存的基礎上進行驗證,這也就是在DetectChanges調用之后調用的原因。通常在驗證類(ValidateEntity)上代碼是不會修改實體上的屬性值,僅僅是進行驗證而已,但是如果改變了其屬性值的話,因此必須手動再一次調用DetectChanges方法來保證修改之后的數據能正確的被保存。有一種可行的方式來對其實體屬性值進行修改,請繼續看接下來的內容。
為什么並不是上下文中所有方法調用DetectChanges方法呢?
在任何時候,只要一個POCO實體可能已經發生改變就調用DetectChanges方法,付出的代價是很顯然的。例如,在每個實體實例化后就執行查詢語句並返回其代碼,換句話說,一個實體查詢要執行很多次。當處理實體時,如果應用程序代碼發生了改變,那么理論上就影響了修改下一個實體的行為。這種情況比較復雜,就不再演示。
看如下代碼:
using (var context = new AnotherBlogContext()) { foreach (var post in context.Posts) { Console.WriteLine(post.Title); } }
如果實體行為發生了改變那么在任何時候就會自動調用DetectChanges方法,如果Posts有1000個,那么將運行這個代碼1000次,那么結果將是EF會調用DetectChanges方法至少1000次。
因此,在每個可能場合調用DetectChanges代價將是非常高的(消耗性能),但是一直不自動調用它,在常見場景下將會導致意想不到和很不直觀的結果,這也就是為什么在常見必須的地方自動調用它,但是不是每個地方都是必須要調用的。因此接下來我們將學習關閉自動調用的DetectChanges以及如何去確定在什么場景下來手動調用它。
關閉自動調用的DetectChanges
如果你的上下文不跟蹤許多實體,你根本就不需要關閉DetectChanges。對於大部分應用程序也是如此,特別是應用程序利用短周期的上下文來作為最佳方式,例如,在每個請求的上下文的web應用程序中。
如果你的上下文跟蹤數千個實體,只要你的應用程序不回調DetectChanges方法很多次,那么通常你也可以不需要關閉自動調用的DetectChanges方法。當你的應用程序正追蹤許多實體並且重復調用要調用DetectChanges方法的方法之一時,這個時候你就得考慮關閉自動調用的DetectChanges了。
最常見的例子就是在一個上下文中循環添加一個集合,如下:
public void AddPosts(List posts) { using (var context = new AnotherBlogContext()) { posts.ForEach( p => context.Posts.Add(p)); context.SaveChanges(); } }
在上述例子中,每一次循環添加一次數據都要造成一次調用DetectChanges,這也就造成了操作執行O(N^2),N為集合中Posts的數量,如果N足夠大,可想而知,付出的代價是很慘淡的,為了避免了此種情況發生,在其潛在一段時間內一直持續某個操作時來手動關閉DetectChanges。如下:
public void AddPosts(List posts) { using (var context = new AnotherBlogContext()) { try { context.Configuration.AutoDetectChangesEnabled = false; posts.ForEach(p => context.Posts.Add(p)); } finally { context.Configuration.AutoDetectChangesEnabled = true; } context.SaveChanges(); } }
*通常使用try/finally來確保自動DetectChanges能重新啟動即使是在添加實體過程中拋出異常。
需要打開或關閉DetectChanges的規則
對於上述情況,你又是怎樣知道不調用DetectChanges是好的呢?進一步講,如果你長時間關閉DetectChanges,你又是怎么確定那是好還是不好呢?
答案是EF需要遵守以下兩條規則
(規則1) 如果之前未被調用,在沒有調用EF代碼將離開上下文狀態的情況下DetectChanges需要被調用
(規則2)在任何時候,如果非EF代碼更改了一個實體或者復雜對象的屬性值,那么此時DetectChanges可能需要被調用
根據規則1上述中的Add將不會導致DetectChanges被調用並且根據規則2在post實體上沒有做出任何改變,因此也不需要調用DetectChanges。事實上,這也意味着,在自動DetectChanges被重新啟動之前,調用SaveChanges也是安全的。
有效利用規則1
如果通過代碼改變了實體上的屬性值而不僅僅是調用Add或者Attach,通過規則2,DetectChanges將會被調用,至少作為SaveChanges的一部分和也有可能在此之前。
然而,通過規則1能被有效避免,當結合DbContext上的屬性API一起使用時,規則1是非常強大的。例如:你想設置一個屬性的值,但是你又不想調用DetectChanges,這個時候你可以通過屬性API來實現,如下:
public void AttachAndMovePosts(Blog efBlog, List posts) { using (var context = new AnotherBlogContext()) { try { context.Configuration.AutoDetectChangesEnabled = false; context.Blogs.Attach(efBlog); posts.ForEach( p => { context.Posts.Attach(p); if (p.Title.StartsWith("Entity Framework:")) { context.Entry(p) .Property(p2 => p2.Title) .CurrentValue = p.Title.Replace("Entity Framework:", "EF:"); context.Entry(p) .Reference(p2 => p2.Blog) .CurrentValue = efBlog; } }); context.SaveChanges(); } finally { context.Configuration.AutoDetectChangesEnabled = true; } } }
上述方法將給出的所有posts集合附加(Attach)到上下文容器中,另外,用EF:開頭的代替了Entity Framework:並且將post轉移到不同的blog。
如果通過代碼直接改變Post實體的屬性值,那么EF將調用DetectChanges來獲取發生的更改並進行修改並且更改的能正確的保存到數據庫。
代替的是,使用上下文中的Property和Reference方法並且設置兩個標題的屬性以及通過使用CurrentValue來設置Blog的導航屬性。意思就是依據規則1,EF確保即使不調用DetectChanges,一切依然正常運行。也就是說,在打開自動DetectChanges之前,會調用SaveChanges使數據都能准確無誤地被保存。
建議
(1)除非你真的需要,建議不要關閉自動DetectChanges,否則將會出現你意想不到的問題
(2)如果你想關閉DetectChanges,請在局部使用try/finally來完成
(3)使用DbContext屬性中的APIs來對實體進行更改而無需調用DetectChanges
此外使用DetectChanges還有一些需要注意的地方,請繼續往下看。
二進制屬性以及復雜類型
DetectChanges和二進制屬性
EF支持二進制屬性來存儲二進制數據。例如,我們在Blog中存儲一張標志圖片,我們通過添加一個屬性到我們的Blog類中。如下:
public byte[] BannerImage { get; set; }
如果你想更改這張圖片,那么你必須通過設置一個新的byte[]實例來實現。不要嘗試去改變已存在二進制數組的內容,因為DetectChanges不會進入到二進制數組里面來看其內容是否已發生了改變。
換言之,將二進制數組當做是不變的,只有重新設置新的二進制數組實例才能進行數據更新。
復雜類型
不懂復雜類型?請看此鏈接Complex Type
實體有一個屬性,該屬性是復雜類型,也就是說有一個復雜的屬性,在此復雜屬性上建立的對象當然也就是復雜對象。
EF允許復雜類型發生突變,也就是說,你能夠改變復雜對象中的屬性值並且EF將檢測這些更改同時與數據庫進行交互,作出適當的更新。
例如:如下,Person實體類帶有一個復雜的屬性Address,同時Address本身包含一個PhoneNumbers的復雜屬性
public class Person { public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Address Address { get; set; } } public class Address { public virtual string Street { get; set; }
public virtual string City { get; set; }
public virtual string State { get; set; }
public virtual PhoneNumbers PhoneNumbers { get; set; } } public class PhoneNumbers { public virtual string Home { get; set; }
public virtual string Work { get; set; } }
如果我們按照如下添加數據,那么你覺得DetectChanges是不必須的?
using (var context = new PeopleContext()) { context.Configuration.AutoDetectChangesEnabled = false; var person = context.People .Single(p => p.Name == "Frans"); person.Address.Street = "1 Tall Street"; person.Address.City = "Fairbanks"; person.Address.State = "AK"; person.Address.PhoneNumbers.Home = "555-555-5555"; person.Address.PhoneNumbers.Work = "555-555-5556"; context.SaveChanges(); }
當你嘗試進行如上操作后,你會發現數據庫中數據沒有任何變化!理由是即使Person被代理了,Addres和PhontNumbers未被代理,即使都有Virtual修飾,EF依然不會為復雜類型創建變更跟蹤代理,但是,快照式變更追蹤和DeteChanges總是應用在復雜類型上。
一切都不會丟失,當使用復雜類型和變更追蹤代理時,你仍然可以避免使用DetectChanges或者說避免需要DetectChanges。只要你將對象當做是不變的,在實際應用中應當認為總是設置一個新的復雜類型的實例,而不是改變已存在實例的屬性的值,例如,修改如下才是正確的
using (var context = new PeopleContext()) { context.Configuration.AutoDetectChangesEnabled = false; var person = context.People .Single(p => p.Name == "Frans"); person.Address = new Address { Street = "1 Tall Street", City = "Fairbanks", State = "AK", PhoneNumbers = new PhoneNumbers { Home = "555-555-5555", Work = "555-555-5556" } }; context.SaveChanges(); }
所以注意:將復雜類型看做是不變的。總是設置一個新的復雜類型的實例,而不是改變已存在復雜類型實例的值。
突變復雜類型而無需DetectChanges
如果你想突變一個復雜對象並且無需調用DetectChanges,你可以通過規則1來進行如下操作:
using (var context = new PeopleContext()) { context.Configuration.AutoDetectChangesEnabled = false; var person = context.People .Single(p => p.Name == "Frans"); var addressEntry = context.Entry(person) .ComplexProperty(p => p.Address); addressEntry .Property(a => a.Street) .CurrentValue = "1 Tall Street"; addressEntry .Property(a => a.City) .CurrentValue = "Fairbanks"; addressEntry .Property(a => a.State) .CurrentValue = "AK"; addressEntry .ComplexProperty(a => a.PhoneNumbers) .Property(p => p.Home) .CurrentValue = "555-555-5555"; addressEntry .ComplexProperty(a => a.PhoneNumbers) .Property(p => p.Work) .CurrentValue = "555-555-5556"; context.SaveChanges(); }
總結
(1)如果在開發中,實體對象為數不對無需過多考慮變更追蹤(DetectChanges)所帶來的細微影響。,但是了解其基本原理和一些注意事項對寫出性能高的代碼也是大有裨益的。
(2)當實體對象較多時,熟悉並運用上面的策略,對其性能的提高可想而知。
(3)吐槽:用EF覺得性能弱爆了的人,其實是自己打自己臉,僅僅關注EF表面實現,而不去了解基本原理就妄下結論。
(4)非常感謝您耐心讀完這一長篇大論。