EF Core 三 、 骚操作 (导航属性,内存查询...)

时间:2023-03-09 09:51:16
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的缓存查询,前面文章已经提到,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);
//处理业务逻辑
//...
}

代码运行效果:

EF Core 三 、 骚操作 (导航属性,内存查询...)

可以看到我们已经能够查询到未提交的数据了,但是也有必须的前提

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();
}
}

结果:

EF Core 三 、 骚操作 (导航属性,内存查询...)

实现了数据插入成功,这里第一个知识点。

如果要实现数据表的关联关系,一对多,必须有如下的约定

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,看下运行效果

EF Core 三 、 骚操作 (导航属性,内存查询...)

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

EF Core 三 、 骚操作 (导航属性,内存查询...)

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; }
}

看下运行效果:

EF Core 三 、 骚操作 (导航属性,内存查询...)

导航属性的几种使用方式还是要结合真正的业务来选择,但是并非所有的场景都要使用,而且要结合性能来考虑,我们来看下导航属性的实现本质

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]

EF Core 三 、 骚操作 (导航属性,内存查询...)

导航属性查询时,会将关联表进行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在使用时还是要多了解,避免使用中带来的更多问题,后续一起继续学习