.NET——關(guān)于EF與延遲加載(LazyLoad)

  • 前一段獨立負責了一個web項目,由于自己一個人開發(fā),技術(shù)選型也更加自由一些,因為項目相對沒那么復雜,拋棄了公司一直沿用的IBatis.Net,用了微軟自己的ORM框架——EF(Entity Framework),在使用的過程中遇到了些許問題,發(fā)現(xiàn)了EF的延遲加載特性,在使用的過程中,深入研究了一下,正好結(jié)合一些基礎(chǔ)知識,做了些深入的學習。
  • 關(guān)鍵字:虛方法 多態(tài)

基本操作

由于項目使用的EF的Code First開發(fā)方式,并且使用Fluent API配置實體類與表的映射,在OnModelCreating方法里通過modelBuilder.Configruations.AddFromAssembly(Assembly.GetExecutingAssembly())加載所有繼承自EntityTypeConfiguration<>的配置類為模型類進行配置。

public class EFDbContext : DbContext
{
     public EFDbContext() : base("name = connstr"){}
     protected override void OnModelCreating(DbModelBuilder modelBuilder)
     {
        base.OnModelCreating(modelBuilder);
        modelBuilder.COnfigurations.AddFromAssembly(Assembly.GetExecutingAssembly());    
     }
}
public class Class
{
    public long Id { get; set; }
    public string Name { get; set; }
}
using System.Data.Entity.ModelConfiguration;
class ClassConfig: EntityTypeConfiguration<Class>
{
    public ClassConfig()
    {
        ToTable("T_Class");
    }
}
  • 當多表之間需要配置主外鍵關(guān)系時,需要在多端進行配置
public class Student
{
    public long Id { get; set; }
    public long ClassId { get; set; }
    public virtual Class Class { get; set; }
    public string Name { get; set; }
}
class StudentConfig : EntityTypeConfiguration<Student>
{
    public StudentConfig()
    {
        this.ToTable("T_Student");
        this.HasRequired(e => e.Class).WithMany().HasForeignKey(e => e.ClassId);
    }
}

調(diào)用情況如下

using (EFContext ctx = new EFContext())
{
    Student s = ctx.Students.First();
    Console.WriteLine(s.SName);
    Console.WriteLine(s.Class.ClassName);
}

打印一下sql調(diào)用過程和結(jié)果如下

EF執(zhí)行SQL:Opened connection at 2019/4/26 9:58:03 +08:00

EF執(zhí)行SQL:SELECT TOP (1)
    [c].[Id] AS [Id],
    [c].[ClassId] AS [ClassId],
    [c].[SName] AS [SName]
    FROM [dbo].[T_Student] AS [c]
EF執(zhí)行SQL:

EF執(zhí)行SQL:-- Executing at 2019/4/26 9:58:03 +08:00

EF執(zhí)行SQL:-- Completed in 0 ms with result: SqlDataReader

EF執(zhí)行SQL:

EF執(zhí)行SQL:Closed connection at 2019/4/26 9:58:03 +08:00

小王
EF執(zhí)行SQL:Opened connection at 2019/4/26 9:58:03 +08:00

EF執(zhí)行SQL:SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[ClassName] AS [ClassName]
    FROM [dbo].[T_Class] AS [Extent1]
    WHERE [Extent1].[Id] = @EntityKeyValue1
EF執(zhí)行SQL:

EF執(zhí)行SQL:-- EntityKeyValue1: '1' (Type = Int64, IsNullable = false)

EF執(zhí)行SQL:-- Executing at 2019/4/26 9:58:03 +08:00

EF執(zhí)行SQL:-- Completed in 1 ms with result: SqlDataReader

EF執(zhí)行SQL:

EF執(zhí)行SQL:Closed connection at 2019/4/26 9:58:03 +08:00

三年二班

通過以上操作可以發(fā)現(xiàn),當分別打印Student和Class的字段時,會兩次調(diào)用SQL語句,這就是EF的延遲加載。

深入延遲加載

延遲加載是EF的默認特性,在平時調(diào)用時,只有通過循環(huán)或者類似ToList()操作時才會真正操作SQL語句。而在Student實體類中,當需要添加Class作為外鍵時,需要添加public virtual Class class{get;set;},如果沒有添加virtual會怎么樣呢?

EF執(zhí)行SQL:Opened connection at 2019/4/26 10:20:48 +08:00

EF執(zhí)行SQL:SELECT TOP (1)
    [c].[Id] AS [Id],
    [c].[ClassId] AS [ClassId],
    [c].[SName] AS [SName]
    FROM [dbo].[T_Student] AS [c]
EF執(zhí)行SQL:

EF執(zhí)行SQL:-- Executing at 2019/4/26 10:20:48 +08:00

EF執(zhí)行SQL:-- Completed in 4 ms with result: SqlDataReader

EF執(zhí)行SQL:

EF執(zhí)行SQL:Closed connection at 2019/4/26 10:20:48 +08:00

小王

未經(jīng)處理的異常:  System.NullReferenceException: 未將對象引用設(shè)置到對象的實例。
   在 ConsoleApp.Program.Main(String[] args) 位置 D:\Work\Code\ConsoleApp\ConsoleApp\Program.cs:行號 37

可以發(fā)現(xiàn),當SQL執(zhí)行完Student的訪問后,去訪問Student里面的Class,然后就報錯了未將對象引用設(shè)置到對象的實例。

簡單分析一下,就很容易發(fā)現(xiàn),在Student類中只是定義了一下Class類型的Class字段,可是用沒有賦值,沒有初始化對象,直接調(diào)用,肯定會報錯,可是為什么加上virtual就可以了呢?

  • 繼續(xù)深入
Student s = ctx.Students.First();
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
Console.WriteLine(s.GetType());
Console.WriteLine(s.Class.GetType());

控制臺:
System.Data.Entity.DynamicProxies.Student_E7E50A6894411395155465B3AA4894D4DAE31B725BE1AD63AA27469E722AB9D3
ConsoleApp.Class

通過分別打印s和s.Class的GetType()可以發(fā)現(xiàn),拿到的對象其實是Student子類的對象,因此EF其實是動態(tài)生成了實體類對象的子類,然后override了這些virtual的屬性,在內(nèi)部進行操作,將Student的Class屬性進行賦值,這樣就解釋的通了。

virtual和override

EF的延遲加載其實用到了C#的多態(tài)性,通過方法重寫和虛方法的調(diào)用,實現(xiàn)了根據(jù)不同的需要,分別加載數(shù)據(jù),之前閱讀過一本書《net 4.0面向?qū)ο缶幊搪劇罚渲杏幸粋€章節(jié)就非常形象的介紹了C#的這一特性。

class Parent
{
    public void HideF()
    {
        Console.WriteLine("Parent.HideF()");
    }
}

class Parent : Parent
{
    public void HideF()
    {
        Console.WriteLine("Child.HideF()");
    }
}
當子類和父類都擁有了一個完全相同的方法HideF,問題發(fā)生了:
Child c = new Child();
c.HideF();      //輸出:Child.HideF()
修改一下代碼:
Parent p = new Parent();
p.HideF();     //輸出:Parent.HideF()

由此得出結(jié)論:當分別位于父類和子類兩個方法完全一樣時,調(diào)用哪個方法由對象變量的類型決定。

繼續(xù)測試:
Parent p = new Child();
p.HideF();     //輸出:Parent.HideF()

繼續(xù)得出結(jié)論:當分別位于父類和子類的兩個方法完全一樣時,調(diào)用哪個方法由對象變量的編譯時類型決定。
如果確實希望調(diào)用的是子類的方法,應(yīng)先進行強制類型轉(zhuǎn)換:
((Child)p).HideF();    //輸出:Child.HideF()

所以,如果父類和子類方法重名,應(yīng)該子類同名方法前加上new關(guān)鍵字public new void HideF(){},
如果要調(diào)用父類的同名方法,可以使用base.HideF();

由于子類隱藏了父類的通過名方法,如果不進行強制轉(zhuǎn)換,就無法通過父類變量直接調(diào)用子類的同名方法,哪怕父類變量引用的是子類對象。

為了達到這個目的,需要在父類同名方法前加關(guān)鍵字virtual,表明這是一個虛方法,子類可以重寫此方法。與此同時,需要在子類同名方法錢加關(guān)鍵字override,表明對父類同名方法進行重寫。

  • 修改之后的結(jié)果
Child c = new child();
Parent p = c;
p.HideF();    //輸出:Child.HideF()

這一示例表明:將父類方法定義為虛方法、子類重寫同名方法后,通過父類變量調(diào)用此方法,到底調(diào)用父類還是子類的方法,由父類變量引用的真實對象類型決定,而與父類變量無關(guān)。

可見,根據(jù)C#的虛方法調(diào)用特性,可以在同樣的語句下,通過引用不同的類型子類對象,就可以實現(xiàn)不同的功能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容