EF6學習筆記十六:變更追蹤
要專業系統地學習EF推薦《你必須掌握的Entity Framework 6.x與Core 2.0》。這本書作者(汪鵬,Jeffcky)的部落格:https://www.cnblogs.com/CreateMyself/
變更追蹤是什麼呢?通過EF持久化資料,那麼EF是怎麼知道你的實體發生了變化,哪裡放生了變化?你可能說是實體狀態,那麼它又是怎麼改變實體狀態呢?
你的POCO實體狀態和EF之間是無法同步的,那麼就需要變更追蹤的機制。
變更追蹤有兩種方式,快照追蹤和代理追蹤
快照追蹤的原理是,EF從資料庫中獲取資料,首先它會在內部對這個實體生成一個快照,也就是副本,另一個則返回給我們程式設計師。
當我們的POCO實體放生變化呼叫savechanges()持久化資料的時候,它會掃描POCO和快照進行對比,更改實體狀態,這樣EF就知道這個實體的變更情況。
這個比較像是MVVM架構的前端框架裡面的雙向繫結,專門寫個方法,去執行這個檢查的操作。EF裡面肯定會複雜一些,首先它有延遲載入,為什麼要有,因為資料是從資料庫中獲取的,要保證資料是新資料,所以越晚給你越好。
第二個方式是代理追蹤,它就沒有建立快照,而是重寫你的POCO生成一個代理類,你的model裡面不是會有virtual修飾的屬性嗎?因為它重寫,動態生成,那麼它就可以加入自己的程式碼。
當你的實體放生改變,那麼不需要掃描去對比,直接就通知EF了。
那麼最後會講到他們之間的效能。你可能會說,既然少了全盤掃描對比,那麼肯定是代理的效能高啊!這可就不一定了,難道代理追蹤的那個通知機制就不耗效能嗎?
我們先歸納一些問題。
1.怎樣識別這兩種方式
2.既然有兩種追蹤方式,那麼到底怎麼使用,是使用了一個就不能使用另一個呢?還是其他
3、EF預設是使用哪一種
4、他們之間的具體區別
5、怎麼實現快照式跟蹤,怎樣實現代理跟蹤
6、全盤掃描,呼叫的是哪個方法
7、代理追蹤和延遲載入的關係
我們可以通過列印上下文的Configuration裡面的具體配置,看看開啟的情況,預設是都開啟的
bool detect = ctx.Configuration.AutoDetectChangesEnabled; bool proxy = ctx.Configuration.ProxyCreationEnabled; Console.WriteLine($"detect:{detect},proxy:{proxy}"); //detect:True,proxy:True View Code
那麼EF到底預設使用哪種方式,我實踐的結論是:快照追蹤
何種方式會建立為代理型別
我們來認識一下代理類,代理類的型別是System.Data.Entity.DynamicProxies後面跟一大串數字+字母
比如我們查詢這個model,我有兩個model,Store商店和Commodity商品,一對多關係。我們看到整個實體和裡面的導航屬性為代理型別。
/// <summary> /// 商品類 /// </summary> public class Commodity : BaseEntity { public string Name { get; set; } public string Unit { get; set; } public decimal Price { get; set; } public string FK_StoreId { get; set; } public virtual Store Store { get; set; } } View Code
Picture
然後我們把Commodity類裡面的virtual去掉,在看一下,我們看到,查詢出的實體不是代理類了,實體中的導航屬性為null
public class Commodity : BaseEntity { public string Name { get; set; } public string Unit { get; set; } public decimal Price { get; set; } public string FK_StoreId { get; set; } public Store Store { get; set; }//去掉vitrual } View Code
Picture
那麼我們現在使用Include載入裡面的導航屬性,可以看到飢餓加載出來的導航屬性為代理型別
Picture
我把Store商店類和BaseEntity貼出來
public class Store:BaseEntity { public string Name { get; set; } public string Address { get; set; } public virtual ICollection<Commodity> Commodities { get; set; } } View Code
public class BaseEntity { public BaseEntity() { this.Id = Guid.NewGuid().ToString(); this.AddTime = DateTime.Now; } public string Id { get; set; } public DateTime AddTime { get; set; } } View Code
現在我們對上一步稍微修改下查詢,我們把Store類中的virtual去掉,我們看到不管是實體本身,還是裡面導航屬性的型別都已經不是代理型別
Picture
我們再來看一下,我有個Book類,它和誰都沒有關係
public class Book:BaseEntity { public string Name { get; set; } public int PageSize { get; set; } } View Code
查詢所有book,沒有代理型別
Picture
那麼為book類中隨便一個屬性加上virtual,再查詢,可以看到沒有代理型別
Picture
既然這樣,那麼我們將Commodity類中的Store屬性去掉virtual,然後在Name屬性用virtual修飾。可以看到,不管查集合還是單個實體,都沒有代理型別
Picture
那麼我們就來得出結論了,如果你的型別裡面的導航屬性(非基元型別的屬性)被vitrual宣告,那麼查詢出的這個實體或者集合就是代理型別,包括導航屬性也是代理型別。
實現快照追蹤與代理追蹤
上面試驗的有點多,可能忘記了前面的問題。上面的測試雖然有代理型別,但是,這就並不代表就是使用的代理追蹤。
因為代理追蹤不會全盤掃描快照,進行對比。那麼EF裡面是哪個方法專管掃描比較這個事情呢?答案就是:DetectChanges()
所以我們只要試驗某一個實體,對他的屬性進行了修改,並且實體狀態放生了變化。如果沒有呼叫DetectChanges()方法就是使用的代理追蹤,如果呼叫了DetectChanges()則是快照追蹤
所以這裡,就要開始除錯EF原始碼了,我今天調了一下,實在是看不懂,但是找到了這個DetectChanges,我在裡面加了句Console
Picture
我們主要面向的是DbContext這個上下文,但是在DbContext上下文中還有一個內部的上下文叫做InternalContext,具體原理不清楚。在內部它使用的的是ObjectContext,而DetectChanges()方法就是在這裡面。比如我們呼叫SaveChanges()方法時,其實他最終會呼叫DetactChanges
不只SaveChanges這一個方法會呼叫DetectChange,還有其他的幾個方法,這幾個方法,作者為我們列出來了
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbContext.Tracker.Entries
先,現在我們來看一下,這是另一個專案,有三個model。和上面的一樣,Order訂單類、Porduct產品類、BaseEntity基類
//基類 public class BaseEntity { public BaseEntity() { this.Id = Guid.NewGuid().ToString(); this.AddTime = DateTime.Now; } public string Id { get; set; } public DateTime AddTime { get; set; } } // 訂單類 public class Order : BaseEntity { publicstring OrderNO { get; set; } publicstring Description { get; set; } public virtual ICollection<Product> Products { get; set; } } //產品類 public class Product : BaseEntity { public string Name { get; set; } public decimal Price { get; set; } public string Unit { get; set; } public string FK_OrderId { get; set; } public virtual Order Order { get; set; } } View Code
那麼我們先新增一個訂單驗證一下,看看Entiry和SaveChanges是不是呼叫了DetectChanges方法。可以看到他呼叫了三次DetectChanges方法,Add一次,SaveChanges一次,Entry一次
Order o = new Order { OrderNO = "order9999", Description = "xxx" }; ctx.Orders.Add(o); ctx.SaveChanges(); Console.WriteLine(ctx.Entry(o).State); View Code
Picture
那我們現在對一個查詢出來的產品資料,修改它的屬性,呼叫Entry獲取它的狀態,如果呼叫了DetectChanges並且狀態改變,那麼就是快照追蹤;如果狀態改變並且沒有呼叫DetectChanges則說明使用的是代理追蹤
var order = ctx.Orders.FirstOrDefault(); Console.WriteLine(ctx.Entry(order).State); order.OrderNO = "dfdf"; Console.WriteLine(ctx.Entry(order).State); View Code
Picture
上面的說明是快照跟蹤。同時也說明了另一個問題,我查詢的是第一個訂單,訂單裡面的產品集合我是用virtual修改是,根據上面的結論這個實體型別和他的導航屬性型別是代理型別,這就說明了,不是有了代理型別,就會使用代理追蹤
代理追蹤需要滿足兩個條件,二者缺一不可,我試了的,各位可以試一下
1、設定:AutoDetectChangesEnabled = false; 關閉自動追蹤
2、將你要設定為代理追蹤的model的所有屬性加上virtual,如果繼承了基類也需要將基類的屬性加上virtual
那麼現在再來看,我們將OrderNo屬性修改,此時這個屬性是被virtual修飾了
ctx.Configuration.AutoDetectChangesEnabled = false; var order = ctx.Orders.FirstOrDefault(); Console.WriteLine(ctx.Entry(order).State); order.OrderNO = "dfdf"; Console.WriteLine(ctx.Entry(order).State); View Code
Picture
可以看到狀態改變,並且沒有呼叫DetectChanges()方法,這就是代理追蹤。
現在我只是實現了代理追蹤的方式,但是對於快照和代理的效能還一無所知,那麼明天我就來簡單弄一下。為什麼說簡單弄一下,因為作者給出的結論就是:這兩種追蹤方式效能上基本沒什麼區別,EF團隊也沒有在代理追蹤上花太多功夫,這是一個沒什麼用的東西。
而且我自己也實在對這個東西不瞭解,面對原始碼完全是懵的。
代理追蹤與延遲載入的關係
最後來看一下代理追蹤和延遲載入的關係,他們之間有什麼關係呢?
首先我們看看下面的查詢,我們關閉代理:ProxyCreationEnabled = false;接著查詢第一個產品
ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.FirstOrDefault(); Console.WriteLine(JsonConvert.SerializeObject(prod,set)); View Code
序列化出的JSON如下,可以看到,導航屬性Order為Null
{ "Name": "柚子", "Price": 5, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Order": null, "Id": "18dec640-b54f-4593-8342-2b7393f8c018", "AddTime": "2019-01-20T12:33:39.53" } View Code
這是為什麼?關閉延遲載入不應該是設定LazyLoadingEnabled = false; 或者將virtual關鍵字去掉嗎?為什麼關閉了代理也無法延遲載入了?
其實延遲載入必須滿足三個條件
1.Configuration.ProxyCreationEnabled = true;
2.Configuration.LazyLoadingEnabled = true;
3.導航屬性修飾符必須為virtual
這就是他們之間的關係,更深入的關係我也說不上來,真遺憾。
其實有一個事比較奇怪,那就是我分別關閉代理和延遲,序列化出來的JSON內容一樣,但是順序不一樣,一起來看下
關閉延遲
//關閉延遲 ctx.Configuration.LazyLoadingEnabled = false; var prod = ctx.Products.Include("Order").First(); Console.WriteLine(JsonConvert.SerializeObject(prod,set)); View Code
{ "Order": { "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "OrderNO": "ttttttt", "Description": "xxx", "AddTime": "2019-01-20T12:33:39.53", "Products": [] }, "Name": "柚子", "Price": 5, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Id": "18dec640-b54f-4593-8342-2b7393f8c018", "AddTime": "2019-01-20T12:33:39.53" } View Code
關閉代理
//關閉代理 ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.Include("Order").First(); Console.WriteLine(JsonConvert.SerializeObject(prod, set)); View Code
{ "Name": "柚子", "Price": 5, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Order": { "OrderNO": "ttttttt", "Description": "xxx", "Products": [], "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "AddTime": "2019-01-20T12:33:39.53" }, "Id": "18dec640-b54f-4593-8342-2b7393f8c018", "AddTime": "2019-01-20T12:33:39.53" } View Code
同時關閉延遲和代理,序列化出來的JSON和僅關閉代理是一樣的
//那我們來同時關閉延遲和代理 ctx.Configuration.LazyLoadingEnabled = false; ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.Include("Order").First(); Console.WriteLine(JsonConvert.SerializeObject(prod,set)); View Code
{ "Name": "柚子", "Price": 5, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Order": { "OrderNO": "ttttttt", "Description": "xxx", "Products": [], "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "AddTime": "2019-01-20T12:33:39.53" }, "Id": "18dec640-b54f-4593-8342-2b7393f8c018", "AddTime": "2019-01-20T12:33:39.53" } View Code
我們上面的都是關閉延遲載入,序列化的結果都是,導航屬性裡面的導航屬性是為空的
那麼如果我不關閉延遲載入,直接使用Netonsoft.json的忽略迴圈引用的配置來序列化,則會發現不一樣。他是導航屬性裡面的導航屬性還有值
//忽略迴圈引用 JsonSerializerSettings set = new JsonSerializerSettings(); set.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; var res = ctx.Products.FirstOrDefault(); Console.WriteLine(JsonConvert.SerializeObject(res,set)); View Code
{ "Order": { "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "OrderNO": "ttttttt", "Description": "xxx", "AddTime": "2019-01-20T12:33:39.53", "Products": [ { "Name": "椪柑", "Price": 3.3, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Id": "3959d99c-ab5f-4c28-a7b4-687337ca205d", "AddTime": "2019-01-20T12:33:39.53" }, { "Name": "橙子", "Price": 4.9, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Id": "cdcb9a8e-1fd2-4ec3-8351-81097b254598", "AddTime": "2019-01-20T12:33:39.53" } ] }, "Name": "柚子", "Price": 5, "Unit": "斤", "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e", "Id": "18dec640-b54f-4593-8342-2b7393f8c018", "AddTime": "2019-01-20T12:33:39.53" } View Code
最後總算要結束了,今天在除錯EF原始碼的時候碰到了一個有意思的東西,我已經制作成了GIF圖。
什麼東西呢?就是我們在除錯程式的時候,比如我們要檢視某一個變數裡面的情況,那麼滑鼠移上去,會出現下拉框顯示關於這個變數的資訊。
可是我碰到的這個還真不是簡單的顯示,你看我這個,我每次移動上去觸發顯示詳情,EF就會發起一次查詢。
Picture