延遲加載(LazyLoad)
如果public virtual Class Class { get; set; }(實體之間的關聯屬性又叫做“導航屬性(Navigation
Property)”)把virtual 去掉,那麼下面的代碼就會報空引用異常
var s = ctx.Students.First();
Console.WriteLine(s.Class.Name);
聯想爲什麼?憑什麼!!!
改成virtual觀察SQL的執行。執行了兩個SQL,先查詢T_Students,再到T_Classes中查到對應的行。
這叫“延遲加載”(LazyLoad),只有用到關聯的對象的數據,纔會再去執行select 查詢。注意延遲加載只在關聯對象屬性上,普通屬性沒這個東西。
注意:啓用延遲加載需要配置如下兩個屬性(默認就是true,因此不需要去配置,只要別手賤設置爲false 即可)
context.Configuration.ProxyCreationEnabled = true;
context.Configuration.LazyLoadingEnabled = true;
分析延遲加載的原理:打印一下拿到的對象的GetType(),再打印一下GetType().BaseType;我們發現拿到的對象其實是Student子類的對象。(如果和我這裏結果不一致的話,說明:類不是public,沒有關聯的virtual 屬性)
因此EF其實是動態生成了實體類對象的子類,然後override了這些virtual屬性,類似於這樣的
實現:
public class StudentProxy:Student
{
private Class clz;
public override Class Class
{
get
{
if(this.clz==null)
{
this.clz= ....//這裏是從數據庫中加載Class 對象的代碼
}
return this.clz;
}
}
}
再次強調:如果要使用延遲加載,類必須是public,關聯屬性必須是virtual。
延遲加載(LazyLoad)的優點:用到的時候才加載,沒用到的時候才加載,因此避免了一次性加載所有數據,提高了加載的速度。缺點:如果不用延遲加載,就可以一次數據庫查詢就可以把所有數據都取出來(使用join實現),用了延遲加載就要多次執行數據庫操作,提高了數據庫服務器的壓力。
因此:如果關聯的屬性幾乎都要讀取到,那麼就不要用延遲加載;如果關聯的屬性只有較小的概率(比如年齡大於7
歲的學生顯示班級名字,否則就不顯示)則可以啓用延遲加載。這個概率到底是多少是沒有一個固定的值,和數據、業務、技術架構的特點都有關係,這是需要經驗和直覺,也需要測試和平衡的。
注意:啓用延遲加載的時候拿到的對象是動態生成類的對象,是不可序列化的,因此不能直接放到進程外Session、Redis 等中,解決方法?
不延遲加載,怎麼樣一次性加
用EF永遠都要把導航屬性設置爲virtual。又想方便(必須是virtual)又想效率高!
使用Include()方法:
var s = ctx.Students.Include("Class").First();
觀察生成的SQL語句,會發現只執行一個使用join的SQL就把所有用到的數據取出來了。當然拿到的對象還是Student 的子類對象,但是不會延遲加載。(不用研究“怎麼讓他返回Student 對象”)
Include(“Class”)的意思是直接加載Student 的Class 屬性的數據。注意只有關聯的對象屬性纔可以用Include,普通字段不可以直接寫"Class"可能拼寫錯誤,如果用C#6.0,可以使用nameof語法解決問這個問題:
var s = ctx.Students.Include(nameof(Student.Class)).First();
也可以using System.Data.Entity;然後var s = ctx.Students.Include(e=>e.Class).First(); 推薦這種做法。
如果有多個屬性需要一次性加載,也可以寫多個Include:
var s = ctx.Students.Include(e=>e.Class) .Include(e=>e.Teacher).First();
如果Class對象還有一個School屬性,也想把School對象的屬性也加載,就要:
var s = ctx.Students.Include("Class").Include("Class. School").First(); 或者更好的
var s = ctx.Students.Include(nameof(Student.Class)).Include(nameof(Student.Class)+"."+nameof(Class.School)).First();
延遲加載的一些坑
-
DbContext銷燬後就不能再延遲加載了,因爲數據庫連接已經斷開
下面的代碼最後一行會報錯:
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.First(); } Console.WriteLine(s.Class.Name);
兩種解決方法:
-
用Include,不延遲加載(推薦)
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.Include(t=>t.Class).First(); } Console.WriteLine(s.Class.Name);
-
關閉前把要用到的數據取出來
Class c; using (MyDbContext ctx = new MyDbContext()) { Student s = ctx.Students.Include(t=>t.Class).First();\ c = s.Class; } Console.WriteLine(c.Name);
-
-
兩個取數據一起使用
下面的程序會報錯:已有打開的與此 Command 相關聯的 DataReader,必須首先將它關閉。
foreach(var s in ctx.Students) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-
因爲EF的查詢是“延遲執行”的,只有遍歷結果集的時候才執行select 查詢,而由於延遲加載的存在到s.Class.Name也會再次執行查詢。ADO.Net中默認是不能同時遍歷兩個DataReader。因此就報錯。
解決方法有如下
-
允許多個DataReader 一起執行:在連接字符串上加上MultipleActiveResultSets=true,但只適用於SQL 2005以後的版本。其他數據庫不支持。
-
執行一下ToList(),因爲ToList()就遍歷然後生成List:
foreach(var s in ctx.Students.ToList()) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-
推薦做法:用Include預先加載:
foreach(var s in ctx.Students.Include(e=>e.Class)) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
-