EF(七)---EF延遲加載

延遲加載(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();

延遲加載的一些坑

  1. 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);
      
  2. 兩個取數據一起使用

    下面的程序會報錯:已有打開的與此 Command 相關聯的 DataReader,必須首先將它關閉。

    foreach(var s in ctx.Students)
    {
        Console.WriteLine(s.Name);
        Console.WriteLine(s.Class.Name);
    }
    
  3. 因爲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);
      }
      
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章