EF Core 三 、 騷操作 (導航屬性,記憶體查詢...)
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
文字就先到這吧,要開始做飯了