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 三 、 骚操作 (导航属性,内存查询...)的更多相关文章

  1. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  2. EF Core 三 、 EF Core CRUD

    EF Core CRUD 上篇文章中,我们已经基本入门了EFCore,搭建了一个简单的EFCore项目,本文开始简单使用下EF,做增删改查的相关操作: 一.数据新增操作(C) public stati ...

  3. EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public?

    前言 不知我们是否思考过一个问题,在关系映射中对于导航属性的访问修饰符是否一定必须为public呢?如果从未想过这个问题,那么我们接下来来探讨这个问题. EF 6.x和EF Core 何种情况下必须配 ...

  4. 《Entity Framework 6 Recipes》中文翻译系列 (24) ------ 第五章 加载实体和导航属性之查询内存对象

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-4  查询内存对象 问题 你想使用模型中的实体对象,如果他们已经加载到上下文中, ...

  5. EF Core 通过延迟加载获取导航属性数据

    EF 6及以前的版本是默认支持延迟加载(Lazy Loading)的,早期的EF Core中并不支持,必须使用Include方法来支持导航属性的数据加载. 当然在EF Core 2.1及之后版本中已经 ...

  6. Entity Framework Core的坑,Select后再对导航属性进行查询或Select前进行Skip/Take

    把asp.net core的项目发布到ubuntu上了,运行的时候出现了如下警告: warn: Microsoft.EntityFrameworkCore.Query[20500] The LINQ ...

  7. 在ASP.NET Core中通过EF Core实现一个简单的全局过滤查询

    前言 不知道大家是否和我有同样的问题: 一般在数据库的设计阶段,会制定一些默认的规则,其中有一条硬性规定就是一定不要对任何表中的数据执行delete硬删除操作,因为每条数据对我们来说都是有用的,并且是 ...

  8. EF Core数据迁移操作

    摘要 在开发中,使用EF code first方式开发,那么如果涉及到数据表的变更,该如何做呢?当然如果是新项目,删除数据库,然后重新生成就行了,那么如果是线上的项目,数据库中已经有数据了,那么删除数 ...

  9. .Net Core 3 骚操作 之 用 Windows 桌面应用开发 Asp.Net Core 网站

    前言 曾经在开发 Asp.Net 网站时就在想,为什么一定要把网站挂到 IIS 上?网站项目的 Main 函数哪儿去了?后来才知道这个 Main 函数在 w3wp.exe 里,这也是 IIS 的主进程 ...

随机推荐

  1. Java蓝桥杯——排列组合

    排列组合介绍 排列,就是指从给定n个数的元素中取出指定m个数的元素,进行排序. 组合,则是指从给定n个数的元素中仅仅取出指定m个数的元素,不考虑排序. 全排列(permutation) 以数字为例,全 ...

  2. Kafka入门之broker-消息设计

    消息设计 1.消息格式 Kafka的实现方式本质上是使用java NIO的ByteBuffer来保存消息,同时依赖文件系统提供的页缓存机制,而非依靠java的堆缓存. 2.版本变迁 0.11.0.0版 ...

  3. 虚拟机下Ubuntu共享文件夹不能显示的一种解决方法

    原文链接:https://blog.csdn.net/huyangzhilin/article/details/70666937

  4. PyQt(Python+Qt)学习随笔:QDockWidget停靠窗相关的信号

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QDockWidget的信号包括与属性变更相关的allowedArea ...

  5. 第十五章 使用PyQt进行Python图形界面程序开发

    在基础知识部分的最后一章<第十三章 Python基础篇结束章>的<第13.3节 图形界面开发tkinter>简单介绍了Python内置图形界面标准库tkinter,当时特别强调 ...

  6. PyQt(Python+Qt)学习随笔:QAbstractItemView的verticalScrollMode和horizontalScrollMode属性

    老猿Python博文目录 老猿Python博客地址 一.概述 verticalScrollMode和horizontalScrollMode属性用于控制视图如何在垂直方向和水平方向滚动内容.滚动可以按 ...

  7. PyQt(Python+Qt)学习随笔:部件的minimumSize、minimumSizeHint之间的区别与联系

    1.minimumSize是一个部件设置的最小值,minimumSizeHint是部件Qt建议的最小值: 2.minimumSizeHint是必须在布局中的部件才有效,如果是窗口,必须窗口设置了布局才 ...

  8. Oracle表操作-创建及增删改查

    数据类型: 1.CHAR:定长字符类型,默认长度是1,最长不超过2000字节. 2.CARCHAR2(length):可变字符类型,默认长度是1,最长不超过4000字符. 3.NUMBER(P,S): ...

  9. Mysql 逻辑架构图及日志系统

    我们经常能看到如下的逻辑架构图,但是往往不能进行很好的记忆,看过就忘记了,也不知道它的实现方式.今天通过简单的画图来简单了解一下mysql到底是如何执行一个select语句,如何update一条语句. ...

  10. java的jdk8新特性optional怎么样使用

    从 Java 8 引入的一个很有趣的特性是 Optional  类.Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException) -- 每个 Java 程序员都 ...