版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/qq1010885678/article/details/38238265
EF(Entity Framework)是微軟的一個ORM架構
使用過EF的同學都知道它有一個延遲加載的功能
那麼這個延遲加載的功能到底是什麼?
為什麼需要延遲加載?
使用延遲加載的優點和缺點又各是什麼?
可以通過一個簡單的小例子來闡述EF的這些問題
首先使用到了兩個很簡單的資料表
關系圖如下:
T_Product的Uid關聯到T_Users的Id,形成一個外鍵關系
是不是真的很簡單= =
然後在測試項目中根據資料庫添加EF資料模型
準備工作已經做好了,現在進入主題
首先需要搞明白的是:什麼是延遲加載?
延遲加載又可以了解成 按需加載
根據字面的意思很容易了解:按照所需的資料 加載資料
上例子:
很簡單的代碼,相信大家都看得懂
接下來設定斷點對代碼的執行步驟進行分解
首先我在如圖所示的位置設定了一個斷點
打開sql server profiler監聽接收到的sql語句
運作程式并命中斷點
此時程式停留在到資料庫取資料(姑且這麼認為)的這行代碼上
由于代碼還未執行是以資料庫不可能接收到任何的sql語句
按下F11逐語句執行
可以看到,程式跳轉,資料庫接收到sql語句
正是var user = dbEntities.T_Users.Where(u => u.Id == 2).FirstOrDefault();這行代碼發送了sql語句到資料庫取的資料
那麼所謂的延遲加載展現在哪裡呢?
其實這行代碼是變相的即使加載,看起來像是調用where方法之後馬上到資料庫取出了資料,但是别忽略了後面的FirstOrDefault()
在EF中,調用where方法隻是先儲存了sql語句,但是并沒有送出到資料庫
但是當調用了FirstOrDefault()方法的時候就表示程式要用資料了,這時sql語句才會被送出到資料庫中
這就是延遲加載,到要使用資料的時候才取出資料
口說無憑,下面繼續用代碼來證明這個說法
将代碼拆分如下,還是在調用where方法的地方設定斷點并運作
F11下一步
這時可以看到,代碼已經執行了where方法,并停在了.FirstOrDefault方法,但是此時資料庫并沒有接收到任何sql語句
為了友善看清楚,在Console.ReadKey()也設定了一個斷點,F5代碼停留在Console.ReadKey()
現在已經很明顯了,隻有程式中要使用資料的時候(調用了FirstOrDefault),sql語句才會被發送發資料庫
這就是EF的延遲加載
但是List集合本身就有一個where方法
難道這個where方法要是延遲加載的嗎,但是這并不是到資料庫取資料的呀
在上圖的智能提示中可以隐約的看到,該where方法的第一個參數是一個IEnumerable的linq表達式
我們可以在var user = dbEntities.T_Users.Where(u => u.Id == 2)這行代碼上對where方法按F12轉到定義看一看
注意看一下這個where方法的傳回值類型,是IQueryable而不是IEnumerable
這說明了兩個where方法雖然名字一樣,甚至連參數都差不多但是一個傳回的是IEnumerable另外一個則是IQueryable
這就從本質上差別開了這兩個where方法
同時注意一下IQueryable的where方法第一個參數的this關鍵字,這說明了什麼?
說明了where并不是在IQueryable接口内部的,而是在外部類通過擴充方法的方式加上去的(這個類就是Queryable,該類中為IQueryable接口提供了n多類似where的擴充方法)
是以可以說,EF能實作延遲加載就是因為有Queryable類的存在
但是從這行代碼中我們又可以看到
var user = dbEntities.T_Users.Where(u => u.Id == 2);
where方法是從dbEntities這個EF上下文對象的T_Users屬性中點出來的
where方法不是在Queryable類中的嗎(或者說是IQueryable接口的)
怎麼就可以從EF的上下文屬性中調用呢?
那麼現在我們就找到EF上下文,看看T_Users屬性到底是怎麼回事
可以很清楚的看到T_Users是一個DbSet<T_Users>類型的泛型屬性
F12轉到定義看一下這個DbSet是什麼東西
是不是能夠在DbSet的基類(接口中)找到IQueryable!
這就解釋了為什麼能夠在EF上下文的T_Users屬性中點出where方法并使用
因為T_Users的類型繼承了IQueryable接口,自然能夠使用IQueryable接口中的方法啦!
上面的長篇大論簡要的解釋了一下EF的延遲加載機制
說了那麼多,那到底為什麼需要延遲加載呢?
直接取直接用不就好了?
确實從這個簡單的例子來看延遲加載貌似很多餘
但是通常我們操作資料庫不可能隻是這麼簡單的
就拿很常用的分頁來說
一般是先對資料進行排序
然後按照要求跳過幾行資料
在取幾行資料
這就不是一個簡單的where方法可以實作的了
至少需要先調用order進行排序,然後skip跳過幾行資料,最後take取幾行資料
如果where/order/skip/take等等方法每次使用的時候就馬上送出sql語句到資料庫
那做一個分頁查詢至少要發送4次請求
也就是說要和資料庫互動4次
如果使用延遲加載的話
上面的where/order/skip/take方法調用的時候可以看做隻是在拼湊條件
當條件滿足的時候(一般就是要用資料的時候,比如說FirstOrDefault方法)
在将整個拼湊好的sql語句一起送出到資料庫
這樣一來和資料庫的互動次數由4降到了1
這就是使用延遲加載的本質原因之一:拼湊條件一起送出,降低資料庫互動次數,提高資料庫的吞吐量
這也是延遲加載的優點
在本例中,T_Products中有一個外鍵Uid關聯到T_Users
上代碼:
var product = dbEntities.T_Products.Where(p => p.Uid == 2).FirstOrDefault();
Console.WriteLine(product.T_Users.UserName);
表内容如下
可以看到Uid為2有兩行
取出Uid為2的T_Products集合然後獲得第一行的實體
并将其關聯的外鍵屬性T_User的UserName輸出
繼續在每行代碼設定斷點并運作
程式停留在上圖的位置,F5執行,命中下一個斷點
可以看到,因為調用了FirstOrDefault方法
是以想資料庫發送了sql語句
但是注意觀察左邊的sql語句
并沒有将外鍵列一起查詢出來
繼續F5運作
還是觀察左邊的sql語句
因為執行了Console.WriteLine(product.T_Users.UserName);這行代碼
需要用到外鍵列
此時EF才向資料庫送出查詢外鍵列的sql語句
這就是延遲加載的本質原因之二:針對外鍵屬性,EF隻有在這個外鍵屬性用到的時候才會去查詢
那麼通過上面的介紹,EF延遲加載的優點已經很明顯了
那麼缺點是什麼呢?
比如說現在的需求是
取出T_Products
var products = dbEntities.T_Products.Where(p => true);
foreach (var p in products)
{
Console.WriteLine(p.T_Users.UserName);
}
Console.ReadKey();
表中的所有實體
周遊輸出每一個實體的外鍵T_Users并輸出其UserName
我們知道
在執行完第一行代碼的時候并沒有和資料庫進行互動
但是此時問題來了
foreach每次一次循環都要用一次products集合中實體的外鍵
這就造成了每循環一次就到資料庫中取一次資料
這就是EF延遲加載的缺點:每次用到外鍵列都會去查詢一次資料庫
其實這裡EF内部已經做了一些列的小優化,對于相同的實體隻取一次,比如說T_Products表中Uid為2的有兩行,Uid為3的有兩行,本來需要查詢4次,經過優化之後隻需要查詢兩次
但是當資料量十分巨大的時候,這些小優化起到的作用微乎其微
那麼要怎麼避免這個缺陷呢?
這時可以通過連接配接查詢先将外鍵列一起查詢出來在循環使用
代碼如下:
var products = dbEntities.T_Products.Include("T_Users");//通過Include告訴EF連接配接查詢哪個外鍵列,特别舒服的事情是,Include可以一直點下去~!
foreach (var p in products)
{
Console.WriteLine(p.T_Users.UserName);
}
Console.ReadKey();
點選運作可以看到下圖的效果
注意左邊的sql語句
生成了inner join的sql語句
隻進行了一次資料庫互動