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
文本就先到这吧,要开始做饭了 ...
EF Core在使用时还是要多了解,避免使用中带来的更多问题,后续一起继续学习
EF Core 三 、 骚操作 (导航属性,内存查询...)的更多相关文章
- 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制
你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...
- EF Core 三 、 EF Core CRUD
EF Core CRUD 上篇文章中,我们已经基本入门了EFCore,搭建了一个简单的EFCore项目,本文开始简单使用下EF,做增删改查的相关操作: 一.数据新增操作(C) public stati ...
- EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public?
前言 不知我们是否思考过一个问题,在关系映射中对于导航属性的访问修饰符是否一定必须为public呢?如果从未想过这个问题,那么我们接下来来探讨这个问题. EF 6.x和EF Core 何种情况下必须配 ...
- 《Entity Framework 6 Recipes》中文翻译系列 (24) ------ 第五章 加载实体和导航属性之查询内存对象
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-4 查询内存对象 问题 你想使用模型中的实体对象,如果他们已经加载到上下文中, ...
- EF Core 通过延迟加载获取导航属性数据
EF 6及以前的版本是默认支持延迟加载(Lazy Loading)的,早期的EF Core中并不支持,必须使用Include方法来支持导航属性的数据加载. 当然在EF Core 2.1及之后版本中已经 ...
- Entity Framework Core的坑,Select后再对导航属性进行查询或Select前进行Skip/Take
把asp.net core的项目发布到ubuntu上了,运行的时候出现了如下警告: warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ ...
- 在ASP.NET Core中通过EF Core实现一个简单的全局过滤查询
前言 不知道大家是否和我有同样的问题: 一般在数据库的设计阶段,会制定一些默认的规则,其中有一条硬性规定就是一定不要对任何表中的数据执行delete硬删除操作,因为每条数据对我们来说都是有用的,并且是 ...
- EF Core数据迁移操作
摘要 在开发中,使用EF code first方式开发,那么如果涉及到数据表的变更,该如何做呢?当然如果是新项目,删除数据库,然后重新生成就行了,那么如果是线上的项目,数据库中已经有数据了,那么删除数 ...
- .Net Core 3 骚操作 之 用 Windows 桌面应用开发 Asp.Net Core 网站
前言 曾经在开发 Asp.Net 网站时就在想,为什么一定要把网站挂到 IIS 上?网站项目的 Main 函数哪儿去了?后来才知道这个 Main 函数在 w3wp.exe 里,这也是 IIS 的主进程 ...
随机推荐
- Java蓝桥杯——排列组合
排列组合介绍 排列,就是指从给定n个数的元素中取出指定m个数的元素,进行排序. 组合,则是指从给定n个数的元素中仅仅取出指定m个数的元素,不考虑排序. 全排列(permutation) 以数字为例,全 ...
- Kafka入门之broker-消息设计
消息设计 1.消息格式 Kafka的实现方式本质上是使用java NIO的ByteBuffer来保存消息,同时依赖文件系统提供的页缓存机制,而非依靠java的堆缓存. 2.版本变迁 0.11.0.0版 ...
- 虚拟机下Ubuntu共享文件夹不能显示的一种解决方法
原文链接:https://blog.csdn.net/huyangzhilin/article/details/70666937
- PyQt(Python+Qt)学习随笔:QDockWidget停靠窗相关的信号
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QDockWidget的信号包括与属性变更相关的allowedArea ...
- 第十五章 使用PyQt进行Python图形界面程序开发
在基础知识部分的最后一章<第十三章 Python基础篇结束章>的<第13.3节 图形界面开发tkinter>简单介绍了Python内置图形界面标准库tkinter,当时特别强调 ...
- PyQt(Python+Qt)学习随笔:QAbstractItemView的verticalScrollMode和horizontalScrollMode属性
老猿Python博文目录 老猿Python博客地址 一.概述 verticalScrollMode和horizontalScrollMode属性用于控制视图如何在垂直方向和水平方向滚动内容.滚动可以按 ...
- PyQt(Python+Qt)学习随笔:部件的minimumSize、minimumSizeHint之间的区别与联系
1.minimumSize是一个部件设置的最小值,minimumSizeHint是部件Qt建议的最小值: 2.minimumSizeHint是必须在布局中的部件才有效,如果是窗口,必须窗口设置了布局才 ...
- Oracle表操作-创建及增删改查
数据类型: 1.CHAR:定长字符类型,默认长度是1,最长不超过2000字节. 2.CARCHAR2(length):可变字符类型,默认长度是1,最长不超过4000字符. 3.NUMBER(P,S): ...
- Mysql 逻辑架构图及日志系统
我们经常能看到如下的逻辑架构图,但是往往不能进行很好的记忆,看过就忘记了,也不知道它的实现方式.今天通过简单的画图来简单了解一下mysql到底是如何执行一个select语句,如何update一条语句. ...
- java的jdk8新特性optional怎么样使用
从 Java 8 引入的一个很有趣的特性是 Optional 类.Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException) -- 每个 Java 程序员都 ...