EF Core 三 、 騷操作 (導航屬性,記憶體查詢...)

五行缺碼發表於2020-12-26

EF Core 高階操作

本文之前,大家已經閱讀了前面的系列文件,對其有了大概的瞭解
我們來看下EF Core中的一些常見高階操作,來豐富我們業務實現,從而擁有更多的實現選擇

1.EF 記憶體查詢

what?我們的ef不是直接連線資料庫嗎?我們查詢的主體肯定是資料庫啊,哪裡來的記憶體呢?
1.所有的資料操作都有過程,並非操作直接會響應到資料庫
2.並非所有的操作都每次提交,會存在快取收集階段,批量提交機制

描述下業務場景,我們存在一個業務,需要儲存一張表,然後還需要對儲存表資料做一些關聯業務處理?我們可能會將方法拆分,首先處理資料儲存,然後再根據資料去處理業務

直接看下程式碼

public static void Query_記憶體查詢()
        {
            TestTable newTable = new TestTable();
            newTable.Id = 10;
            newTable.Name = "測試資料";
            using (MyDbContext dbContext = new MyDbContext())
            {
                dbContext.Add(newTable);
                Query_記憶體查詢_關聯業務處理(dbContext);
                dbContext.SaveChanges();
            }
        }

        private static void Query_記憶體查詢_關聯業務處理(MyDbContext dbContext)
        {
            var entity = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
            //處理業務邏輯
            //...
        }

程式碼執行效果:

發現並沒有將資料查詢出來,因為預設會查詢資料庫資料,此時資料還未提交,所以無法查詢。但是也可以將實體資料傳入到依賴方法啊,這樣可以解決,但是如果關聯實體多,來回傳遞麻煩,所以這不是最佳解

EF Core的快取查詢,前面文章已經提到,EF Core會將所有的改動儲存到本地的快取區,等待一起提交,並隨即提供了基於快取查詢的方法,我們來驗證下

public static void Query_記憶體查詢()
        {
            TestTable newTable = new TestTable();
            newTable.Id = 10;
            newTable.Name = "測試資料";
            using (MyDbContext dbContext = new MyDbContext())
            {
                dbContext.Add(newTable);
                Query_記憶體查詢_關聯業務處理(dbContext);
            }
        }
        private static void Query_記憶體查詢_關聯業務處理(MyDbContext dbContext)
        {
            var entity = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
            //處理業務邏輯
            //...
            var entity2 = dbContext.TestTables.Find(10);
            //處理業務邏輯
            //...
        }

程式碼執行效果:

可以看到我們已經能夠查詢到未提交的資料了,但是也有必須的前提
1.必須使用ID查詢,這點我們下面來分析
2.必須保證在同一上下文中,這點通過我們前面文章分析,快取維護都是基於上下文維護,所以無法跨上下文來實現快取資料查詢

直接看原始碼,通過原始碼檢視,分析得到通過Find()方法呼叫StateManager.FindIdentityMap(IKey key)方法

private IIdentityMap FindIdentityMap(IKey key)
        {
            if (_identityMap0 == null
                || key == null)
            {
                return null;
            }

            if (_identityMap0.Key == key)
            {
                return _identityMap0;
            }

            if (_identityMap1 == null)
            {
                return null;
            }

            if (_identityMap1.Key == key)
            {
                return _identityMap1;
            }

            return _identityMaps == null
                   || !_identityMaps.TryGetValue(key, out var identityMap)
                ? null
                : identityMap;
        }

這裡就是對_identityMaps集合進行查詢,那這個集合是什麼時候有資料呢?為何新增的資料會在?看下DBContext.Add方法
DbContext.Add=>InternalEntityEntry.SetEntityState=> StateManager.StartTracking(this)=>StateManager.GetOrCreateIdentityMap
核心程式碼:

  if (!_identityMaps.TryGetValue(key, out var identityMap))
            {
                identityMap = key.GetIdentityMapFactory()(SensitiveLoggingEnabled);
                _identityMaps[key] = identityMap;
            }

會將當前實體放入集合中,如果集合中沒有查詢到,那就會執行資料庫查詢命令

2.導航屬性

通過一個實體的屬性成員,可以定位到與之有關聯的實體,這就是導航的用途了
業務的發生永遠不會堆積在單表業務上,可能會衍生多個關聯業務表上,那在這種場景下,我們就需要導航屬性,還是以示例入手

首先,我們需要兩個關聯實體,來看下實體

[Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] 
        public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key] 
        public int Id { get; set; }              
        public int TestTableId { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }        
    }

然後我們來測試下,實現關聯資料的插入

public static void Insert_導航屬性_資料準備()
        {
            TestTable table = new TestTable();
            table.Id = 10;
            table.Name = "主表資料10";
            TestTableDetail detail1 = new TestTableDetail();
            detail1.Id = 1;
            //detail1.PID = 10;
            detail1.Name = "主表資料10-從表資料1";
            TestTableDetail detail2 = new TestTableDetail();
            detail2.Id = 2;
            //detail2.PID = 10;
            detail2.Name = "主表資料10-從表資料2";
            table.TestTableDetails = new List<TestTableDetail>();
            table.TestTableDetails.Add(detail1);
            table.TestTableDetails.Add(detail2);
            using (MyDbContext db = new MyDbContext())
            {
                if (db.TestTables.FirstOrDefault(p => p.Id != 10) == null)
                    return;
                db.TestTables.Add(table);
                //db.TestTableDetails.Add(detail1);
                //db.TestTableDetails.Add(detail2);
                db.SaveChanges();
            }
        }

結果:

實現了資料插入成功,這裡第一個知識點。
如果要實現資料表的關聯關係,一對多,必須有如下的約定
1.EFCore 預設導航屬性,約定規則,主表包含從表資料集合,且從表包含主表表明+'Id'的欄位
這樣主,從表會被EFCore預設識別到,自動維護從表的外來鍵資訊
2.主實體包含從列表實體,以及從實體包含主實體,且從表包含從表導航屬性名+主表主鍵名

 [Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key] 
        public int Id { get; set; }                      
        public int PID { get; set; }
        public string Name { get; set; }   

        public int TestId { get; set; }
        public TestTable Test { get; set; }
    }

TestTableDetail中包含了導航屬性Test,主實體主鍵為ID,那就必須包含外來鍵TestId,看下執行效果

3.從實體包含導航屬性,且包含 主表名稱+主表主鍵 的外來鍵欄位

 [Table("TestTable")]
    public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key] 
        public int Id { get; set; }                      
        public int PID { get; set; }
        public string Name { get; set; }   

        public int TestTableId { get; set; }
        public TestTable Test { get; set; }
    }

三面三種方式來建立我們實體之間的主外來鍵關係也還不錯,但是往往業務中可能沒有我們想象的簡單,沒法符合上面的三種規則,那我們就需要手動來設定導航屬性
4.手動設定一,實體ForeignKey設定

public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key] 
        public int Id { get; set; }                      
        public int PID { get; set; }
        public string Name { get; set; }  
        [ForeignKey("PID")]
        public TestTable Test { get; set; }
    }

執行結果,可以看到我們使用了自定義的外來鍵PID

5.手動設定二,Fluent API 設定
DbContext配置實體關係

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {                      
            // 對映實體關係,一對多
            modelBuilder.Entity<TestTableDetail>()
                        .HasOne(p=>p.Test)
                        .WithMany(p=>p.TestTableDetails)
                        .HasForeignKey(p=>p.PID);
        }        
 public class TestTable : EntityBase
    {
        [Key] public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key] 
        public int Id { get; set; }                      
        public int PID { get; set; }
        public string Name { get; set; }     
        public TestTable Test { get; set; }
    }

看下執行效果:

導航屬性的幾種使用方式還是要結合真正的業務來選擇,但是並非所有的場景都要使用,而且要結合效能來考慮,我們來看下導航屬性的實現本質

public static void Query_導航屬性()
        {
            MyDbContext dbContext = new MyDbContext();
            var test = dbContext.TestTables.Where(p=>p.Id==10).
                Include(c => c.TestTableDetails).FirstOrDefault();
        }

通過API Include方法,來執行導航屬性查詢,然後跟蹤SQL如下

SELECT [t0].[Id], [t0].[Name], [t1].[Id], [t1].[Name], [t1].[PID]
FROM (
    SELECT TOP(1) [t].[Id], [t].[Name]
    FROM [TestTable] AS [t]
    WHERE [t].[Id] = 10
) AS [t0]
LEFT JOIN [TestTableDetail] AS [t1] ON [t0].[Id] = [t1].[PID]
ORDER BY [t0].[Id], [t1].[Id]


導航屬性查詢時,會將關聯表進行Left Join,返回一張寬表,包含兩張表的全部欄位,主表資料量會呈現翻倍增長
例如:主表資料1條,二級從表3條,三級從表每個10條,那就是一張三十條資料的大寬表,從資料查詢以及傳輸來看,對效能會照成比較大的影響,所以一定要慎用
有以下幾個點:
1.在不需要關聯表資料時,不需要使用Include,只會查詢出主表資料

var test1 = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);

2.那如果可能需要關聯表資料呢?能夠有一種方法,在我需要關聯資料的時候再去查詢?
-- 2.1 分段查詢,我們來看下具體效果

public static void Query_導航屬性()
        {
            MyDbContext dbContext = new MyDbContext();           
            //定義查詢條件,並不會執行資料庫查詢
            var query = dbContext.TestTables.Where(p => p.Id == 10);
            //執行查詢,但是隻會查詢主表資料         
            var test4 = query.FirstOrDefault();
            //需要從表資料時,再觸發查詢
            query.SelectMany(p => p.TestTableDetails).Load();
        }

第一次查詢

SELECT [t].[Id], [t].[Name]
FROM [TestTable] AS [t]
WHERE [t].[Id] = 10

第二次查詢

SELECT [t0].[Id], [t0].[Name], [t0].[PID]
FROM [TestTable] AS [t]
INNER JOIN [TestTableDetail] AS [t0] ON [t].[Id] = [t0].[PID]
WHERE [t].[Id] = 10

第一次只會查詢主表,第二次查詢通過Inner Join,效能也遠高於Left join,且只返回了TestTableDetail的資料

-- 2.2 Linq to SQL 或者 Lambda Join()
通過自主決定查詢資料來優化查詢方式,來提高查詢效率,這也是決定Left join或者Inner join的一種方式
兩種方式在特定場景下還是有比較大的效能差異

left join(左聯接) 返回包括左表中的所有記錄和右表中聯結欄位相等的記錄   
right join(右聯接) 返回包括右表中的所有記錄和左表中聯結欄位相等的記錄  
inner join(等值連線) 只返回兩個表中聯結欄位相等的行
關於left join的概念,left join(返回左邊全部記錄,右表不滿足匹配條件的記錄對應行返回null),那麼單純的對比邏輯運算量的話,inner join 是隻需要返回兩個表的交集部分,left join多返回了一部分左表沒有返回的資料。sql儘量使用資料量小的表做主表,這樣效率高,但是有時候因為邏輯要求,要使用資料量大的表做主表,此時使用left join 就會比較慢,即使關聯條件有索引。在這種情況下就要考慮是不是能使用inner join 了。因為inner join 在執行的時候回自動選擇最小的表做基礎表,效率高.

-- 2.3 延遲載入
1.使用 Proxies代理方式
引入Microsoft.EntityFrameworkCore.Proxies包
2.註冊代理

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLazyLoadingProxies();
            //寫入連線字串
            optionsBuilder.UseSqlServer("Data Source=.\\SQLSERVER;Initial Catalog=EfCore.Test;User ID=sa;Pwd=123");            
        }

3.修改實體,導航屬性增加 virtual 關鍵字

[Table("TestTable")]
    public class TestTable : EntityBase
    {       
        [Key] public int Id { get; set; }
        public string Name { get; set; }        
        public virtual ICollection<TestTableDetail> TestTableDetails { get; set; }
    }

    [Table("TestTableDetail")]
    public class TestTableDetail : EntityBase
    {
        [Key]
        public int Id { get; set; }
        public int PID { get; set; }
        public string Name { get; set; }
        public virtual TestTable Test { get; set; }
    }

然後直接執行查詢即可

var test1 = dbContext.TestTables.FirstOrDefault(p => p.Id == 10);
var count = test1.TestTableDetails.Count();

觀察SQL
第一次:

SELECT TOP(1) [t].[Id], [t].[Name]
FROM [TestTable] AS [t]
WHERE [t].[Id] = 10

第二次,訪問TestTableDetails時觸發

exec sp_executesql N'SELECT [t].[Id], [t].[Name], [t].[PID]
FROM [TestTableDetail] AS [t]
WHERE [t].[PID] = @__p_0',N'@__p_0 int',@__p_0=10

文字就先到這吧,要開始做飯了 ? ...
EF Core在使用時還是要多瞭解,避免使用中帶來的更多問題,後續一起繼續學習

相關文章