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 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();
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 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 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 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()
.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 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 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在使用时还是要多了解,避免使用中带来的更多问题,后续一起继续学习
打赏
微信扫一扫,打赏作者吧~