efcore 批量_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 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在使用时还是要多了解,避免使用中带来的更多问题,后续一起继续学习

打赏

微信扫一扫,打赏作者吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值