.net EF Core专题:EF Core 读取数据时发生了什么?

原文:https://bit.ly/2UMiDLb
作者:Jon P Smith
翻译:王亮
声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的。其中可能会去除一些本人实在不知道如何组织但又不影响理解的句子。

本文将为你详细描绘 EF Core 从数据库中读取数据的“幕后”视图。我将揭开两种数据库读取方式的面纱:一个是普通的查询,另一个是使用 AsNoTracking 方法的非跟踪查询。我还将通过一个实验来演示我是如何解决我的一个客户遇到的性能问题。

我假设你对 EF Core 已经有了一定的认识,但在深入学习之前,我们先来了解一下如何使用 EF Core,以确保我们已经掌握了一些基本知识。这是一个“深入研究”的课题,所以我准备大量的技术细节,希望我的描述方式你能理解。

本文是“深入理解 EF Core”系列中的第一篇。以下是本系列文章列表:

  • 当 EF Core 从数据库读取数据时发生了什么?(本文)
  • 当 EF Core 写入数据到数据库时发生了什么?(敬请期待)

概要

  • EF Core 有两种方法从数据库中读取数据(也称为查询):普通 LINQ 查询和包含 AsNoTracking 方法的非跟踪 LINQ 查询。
  • 这两种方法查询的返回类(被称为实体类),它连接的其它的实体类(即所谓的导航属性)也被同时加载,但这两种法如何连接及连接的内容是不一样的。
  • 普通查询接受的是 DbContext 执行读取时所有数据的副本——此时的实体类称为被跟踪。这允许加载的实体类参与数据库的更新操作。
  • 普通查询还会有一些其它的复杂底层实现,称为关系修补(fixup),用于描述读入的实体类和其他被跟踪实体之间的连接关系。
  • AsNoTracked 非跟踪查询没有副本,所以它没有被跟踪——这意味着它比普通查询更快。这也意味着它不会用于数据库的写操作。
  • 最后,我将展示 EF Core 普通查询中一个鲜为人知的特性,以此作为示例,说明通过导航属性连接实体类的关系是多么智能。

EF Core 如何读取数据库数据

提示:如果你已经对 EF Core 有一定的认识,那么你可以跳过这一节,这部分只是一个如何读取数据库的例子。

为了能让你更好地理解,我先描述一个数据库结构,然后再给出一个简单的数据库读取示例。下面是一些基本表的结构和它们之间的关系。

这些表被映射到具有类似名称的类,例如 Book、BookAuthor、Author,这些类的属性名称与表的字段名称相同。由于篇幅有限,我不打算展开来讲这些类,但您可以在我的 GitHub 仓库[1]中查看这些类。

EF Core 读取数据库需要下面五部分:

  1. 数据库服务器,如 SQL server, Sqlite, PostgreSQL 等。
  2. 具有数据的数据库。
  3. 映射到数据表的类(称为实体类)。
  4. 一个继承 DbContext 的类,该类包含 EF Core 的配置。
  5. 最后,从数据库读取数据的命令。

下面的单元测试代码来自我的 GitHub 创库[2],展示了一个简单的示例,它从现有数据库中读取 4 个 Book 实体及其关联的 BookAuthor 和 Authors 实体。

仓库地址:https://bit.ly/2Yza7QQ

[Fact]
public void TestBookCountAuthorsOk()
{
  //SETUP
  var options = SqliteInMemory.CreateOptions<EfCoreContext>();
  //code to set up the database with four books, two with the same Author
  using (var context = new EfCoreContext(options))
  {
    //ATTEMPT
    var books = context.Books
      .Include(r => r.AuthorsLink)
      .ThenInclude(r => r.Author)
      .ToList();

    //VERIFY
    books.Count.ShouldEqual(4);
    books.SelectMany(x => x.AuthorsLink.Select(y => y.Author))
      .Distinct().Count().ShouldEqual(3);
  }
}

现在,如果我们将单元测试代码对应到上面的 5 部分,结果是这样的:

  1. 数据库服务器——第 5 行:我选择了一个 Sqlite 数据库服务器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用我的一个 NuGet 包 EfCore.TestSupport 创建了一个内存数据库(内存中的数据库对于单元测试非常有用,因为你可以为这个测试建立一个新的空数据库)。
  2. 具有数据的数据库——第 6 行:我将在下一篇文章介绍数据是如何写入数据库的,现在假设有一个数据库包含 4 本书信息,其中两本书的作者是同一个人。
  3. 实体类——代码里这里没有展示,但是你可以在这里查看这些类[1]。其中有一个 Books 实体类,通过一个名为 BookAuhor 的实体类多对多关联 Authors 实体类。
  4. 一个继承 DbContext 的类——第 7 行:EfCoreContext 类继承了 DbContext 类并配置了从类到数据库的映射关系(你可以在我的 GitHub 仓库[3] 中查看该类)。
  5. 从数据库读取数据的命令——第 10 到 13 行,这是一个查询:
  • 第 10 行 — context 为 EfCoreContext 的实例,通过它访问你的数据库,.Books 表示您希望访问 Books 表。
  • 第 11 行 — Include 被称为贪婪加载,它告诉 EF Core 当它加载 Books 时,也应该加载关联到的所有 BookAuthor 实体类。
  • 第 12 行 — ThenInclude 是继续贪婪加载,它告诉 EF Core 当它加载一个 BookAuthor 时,它也应该加载关联到该 BookAuthor 的 Author 实体类。

所有这一切查询出来是一个结果集,其中有普通属性,像 Books 的 Title 属性;有关联实体类的导航属性,像 Books 的 AuthorsLink 属性。

这个示例称为查询或读取,也是四种数据库访问类型之一,即 CRUD(新增、读取、更新和删除)。我将在下一篇文章中介绍新增和更新。

EF Core 如何表示读取的数据

当你查询数据库时,EF Core 会将数据库返回的数据转换为实体类并填充导航属性的值。在本节中,我们将研究两种类型的查询步骤——普通查询(即没有 AsNoTracking 方法,也称为读写查询)和添加了 AsNoTracking 方法的非跟踪查询(称为只读查询)。

我们先来看一下最初 LINQ 语句是如何转换成数据库相应的查询命令然后返回数据的。对于我们将要看到的两种类型的查询来说,这是很常见的操作。关于查询的第一部分,请参见下图。

有一些非常复杂的代码将你的 LINQ 转换为数据库查询命令,但这些内部细节我们不必关心。如果你的 LINQ 不能被翻译,你会从 EF Core 得到一个异常消息,其中包含类似“不能被翻译”的描述词语。此外,当数据返回时,像 Value Converters[4] 这样的特性可能会调整数据。

本节展示了查询的第一部分,其中 LINQ 被转换为数据库命令并返回所有正确的值。现在我们来看查询的第二部分,在这里 EF Core 获取返回值并将它们转换为实体类的实例,并填充导航属性。我们将分别看看两种类型的查询。

1. 普通查询(读写查询)

普通查询读取数据的方式可以修改数据并更新到数据库,这就是我将其称为读写查询的原因。它不会自动更新数据(请参阅下一篇文章,了解如何写入数据库)。如果你要更新数据,你的查询必须是读写查询。

我在介绍中给出的示例执行的是一个普通读写查询,读取带有 AuthorsLink 实例的示例。下面是该示例的查询部分的代码:

var books = context.Books
  .Include(r => r.AuthorsLink)
  .ThenInclude(r => r.Author)
  .ToList();

然后 EF Core 通过三个步骤将这些值转换并填充含有导航属性的实体类。下图显示了这三个步骤以及生成的实体类及其导航属性的实体类。

让我们来分析一下这三个步骤:

  1. 创建类并填充数据。它接受数据库返回的值,并填充非导航(称为标量)属性、字段等。在 Book 实体类中,是 BookId(主键)、Title 等属性——参见上图左下角浅蓝色矩形。
  2. 修补关联关系。首先是填入主键和外键的信息,它们定义如何相互关联数据。然后,EF Core 使用这些键设置实体类之间的导航属性(如图中蓝色粗线所示)。这个关系的修补所需的信息不仅是查询读入的实体类,它还会查看 DbContext 中跟踪的每个实体,并填充导航属性。这是一个强大的功能,但你的被跟踪实体越多,所需消耗时间也越多——这就是为什么需要 AsNoTracking 来实现更快的查询。
  3. 创建跟踪快照。跟踪快照是返回给用户的实体类的一个副本,加上它所隐藏的与每个实体类的关联关系——若一个实体处于被跟踪状态,这意味着它将会发生修改并会写入到数据库中。

2. 非跟踪查询(只读查询)

非跟踪查询,即使用 AsNoTracking 方法的查询,是一个只读查询。这意味着,当 SaveChanges 方法被调用时,你读取的任何内容都不会被写入数据库。非跟踪查询的查询效率更高,在下一节中,我将介绍非跟踪查询以及与普通查询的其他区别。

在前文的示例之后,我修改了查询代码,添加了下面的 AsNoTracking 方法(请看第 2 行):

var books = context.Books
  .AsNoTracking()
  .Include(r => r.AuthorsLink)
  .ThenInclude(r => r.Author)
  .ToList();

这里的 LINQ 查询只有上面的普通查询的前两个步骤(没有第三个步骤)。下图显示了 AsNoTracking 查询的步骤。

步骤如下:

  1. 创建类并填充数据。它接受数据库返回的值,并填充非导航(称为标量)属性、字段等。在 Book 实体类中,是 BookId(主键)、Title 等属性——参见上图左下角浅蓝色矩形。
  2. 修补关联关系。首先是填入主键和外键的信息,它们定义如何相互关联数据。然后,EF Core 使用这些键设置实体类之间的导航属性(如图中蓝色粗线所示)。这个关系的修补所需的信息不仅是查询读入的实体类,它还会查看 DbContext 中跟踪的每个实体,并填充导航属性。这是一个强大的功能,但你的被跟踪实体越多,所需消耗时间也越多——这就是为什么需要 AsNoTracking 来实现更快的查询。

普通查询和非跟踪查询的区别

现在让我们比较这两种查询比较明显的区别。

  1. 非跟踪查询查询的性能更好。使用非跟踪查询查询的主要原因是性能。非跟踪查询查询表现为:
  • 稍微快一点,使用的内存稍微少一点,因为它不需要创建跟踪快照。
  • 避免没有必要的跟踪快照可以提高 SaveChanges 的性能,因为它不必检查跟踪快照以查找更改。
  • 稍微快一点,因为修补关联关系时没有所谓的身份解析。这就是为什么你会得到两个具有相同数据的 Author 实例。
  1. 非跟踪查询修补关联关系时只链接查询中的实体。在普通查询中,我已经说过修补关联关系时连接的是查询中的实体和当前跟踪的实体,但是非跟踪查询只修补查询中的实体关系。
  2. 非跟踪查询并不总是代表数据库关系。这两种类型查询之间的关系修补的另一个区别是,非跟踪查询关系修补更快,它不需要标识的解析。这可以为数据库中的同一行生成多个实例——见上图右下角蓝色的 Author 实体和注释。如果只是向用户显示数据,那么这种差异并不重要,但是如果具有业务逻辑,那么多个实例不能正确反映数据的结构,就可能会有问题。

对层级数据有用的关系修补特性

关联关系修补的步骤是非常智能的,特别是在普通查询中。下面我想向你展示我是如何利用关系修补的特性来解决一个客户项目中的性能问题的。

我曾在一家公司工作,那里的许多数据处理都是层次化结构的,即数据具有一系列深度不确定的关联关系。问题是我必须先解析整个层次结构,然后才能呈现这些数据。我最初是通过贪婪的方式加载前两个层级,然后显式地加载更深的层级来实现这一点的。它可以工作,但是性能非常慢,并且数据库因大量单数据库访问而超载。

这不得不让我思考解决办法,如果普通查询的关系修补那么智能的话,它能帮助我提高查询的性能吗?它可以!让我给你举一个公司员工的例子。下图显示了我们想要加载的公司的层次结构。

你可以接龙式地使用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe)… 等等来加载所需的层级信息,但结果是一个 .Include(x => x.WorksForMe) 就够了。因为 EF Core 的关系修补为你做了剩下的事情,这一点很惊奇,但也很有用。

例如,如果我想查询角色为 Development 的所有员工(每个员工都有一个名为 WhatTheyDo 的属性和名为 Role 的属性,该 Role 包含他们工作的部门),我可以这样编写代码:

var devDept = context.Employees
  .Include(x => x.WorksFromMe)
  .Where(x => x.WhatTheyDo.HasFlag(Roles.Development))
  .ToList();

这将创建一个查询,用于加载角色为 Development 的所有员工,并且在员工实体类上修补与 WorksFoMe 导航属性(集合)和 Manager 导航属性(单个)的关系。通过只执行一个查询,既提高了查询花费的时间,又减少了数据库服务器上的负载。

总结

你已经看到了两种类型的查询,我称之为 a)普通的读写查询,和 b) 非跟踪的只读查询。对于每一种查询类型,我都向你展示了 EF Core “幕后”是如何读取数据并展示的。他们工作方式的不同也表现出他们的优势和劣势。

非跟踪查询是只读查询的解决方案,因为它比普通读写查询更快。但是您应该记住关系修补的机制,它可以在数据库只有一个关系的情况下创建类的多个实例。

普通的读写查询是查询跟踪实体的解决方案,这意味着你可以在创建、更新和删除数据时使用它们。普通的读写查询确实会占用更多的时间和内存资源,但是有一些有用的特性,比如自动链接到其他被跟踪的实体类实例。

我希望这篇文章对您有用。祝你编程快乐!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO
[4]. https://bit.ly/2YEyg8j

(0)

相关推荐

  • ASP.NET TreeView读取数据库实例

    前台: 复制代码 代码如下: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="TreeView._Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:

  • asp.net连接数据库读取数据示例分享

    webconfig配置: 复制代码 代码如下: <connectionStrings>  <add name="MSSQL" connectionString="Data Source=localhost;Initial Catalog=test;User ID=sa;password=sa;" providerName="System.Data.SqlClient"/></connectionStrings>

  • 从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入

    故事背景 最近在把自己的一个老项目从Framework迁移到.Net Core 3.0,数据访问这块选择的是EFCore+Mysql.使用EF的话不可避免要和DbContext打交道,在Core中的常规用法一般是:创建一个XXXContext类继承自DbContext,实现一个拥有DbContextOptions参数的构造器,在启动类StartUp中的ConfigureServices方法里调用IServiceCollection的扩展方法AddDbContext,把上下文注入到DI容器中,然后

  • asp.net实现存储和读取数据库图片

    本文实例为大家分享了asp.net存储和读取数据库图片的具体代码,供大家参考,具体内容如下 1. 创建asp.net web窗体项目 代码如下: <body> <form id="form1" runat="server"> <h2>上传图片</h2> <div> <asp:FileUpload ID="ful_image" runat="server" /&g

  • asp.net上传Excel文件并读取数据的实现方法

    前言 本文主要给大家介绍了关于asp.net上传Excel文件并读取数据的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 实现如下: 前台代码:使用服务端控件实现上传 <form id="form1" runat="server"> <div> <asp:FileUpload ID="ExcelFileUpload" runat="server" /> <as

  • .net EF Core专题:EF Core 读取数据时发生了什么?

    原文:https://bit.ly/2UMiDLb 作者:Jon P Smith 翻译:王亮 声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的.其中可能会去除一些本人实在不知道如何组织但又不影响理解的句子. 本文将为你详细描绘 EF Core 从数据库中读取数据的"幕后"视图.我将揭开两种数据库读取方式的面纱:一个是普通的查询,另一个是使用 AsNoTracking 方法的非跟踪查询.我还将通过一个实验来演示我是如何解决我的一个客户遇到的性能问题. 我假设你对 EF C

  • Ajax读取数据之分页显示篇实现代码

    我觉得还是有必要把我的ajax分页呈现给大家.我先讲一下这个ajax分页的实现核心,然后我们再看实例效果.所谓的ajax分页与传统的数据分页,在服务端的代码基本上是一样的.我们主要做的是使用ajax在不刷新的情况下,将请求的页码,和每页要显示的条数发送给服务端的处理网页.大家点击这个网址就可以看到要请求的数据:"ajax_page.asp?action=read&pagecount=3 & current_page=1" 在这段URL里,pagecount代表每页要显示

  • pandas读取csv格式数据时header参数设置方法

    目录 写在前面 参考文档 read_csv的header参数 header参数测试 思考 写在前面 使用pandas中read_csv读取csv数据时,对于有表头的数据,将header设置为空(None),会报错:pandas_libs\parsers.pyx in pandas._libs.parsers.raise_parser_error() ParserError: Error tokenizing data. C error: Expected 4 fields in line 2,

  • ASP.NET Core使用EF查询数据

    1.查询生命周期 在进入正题时候,我们先来了解EF Core查询的生命周期. 1.1LINQ查询会由Entity Framework Core处理并生成给数据库提供程序可处理的表示形式(说白了就是生成给数据库可识别数据形式). 发送的查询结果(查询表示形式)会被缓存,以便每次执行查询时无需进行1.1中处理. 1.2查询结果(查询表示形式)会传递到数据库提供程序 数据库提供程序会识别出查询的哪些部分可以在数据库中求值. 查询的这些部分会转换为特定数据库的查询语言(例如,关系数据库的T-SQL).

  • ASP.NET Core使用EF保存数据、级联删除和事务使用

    目录 1.简介 2.基本保存 2.1添加数据 2.2更新数据 2.3删除数据 2.4单个SaveChanges中的多个操作 3.保存关联数据 3.1添加关联数据 3.2添加相关实体 3.3更改关系 4.级联删除 5.事务 5.1控制事务 1.简介 每个上下文实例都有一个ChangeTracker,它负责跟踪需要写入数据库的更改.更改实体类的实例时,这些更改会记录在ChangeTracker中,然后在调用SaveChanges时会被写入数据库中.此数据库提供程序负责将更改转换为特定于数据库的操作(

  • ASP.NET Core使用EF创建模型(索引、备用键、继承、支持字段)

    目录 1.索引 1.1约定 1.2数据批注 1.3Fluent API 2.备用键 2.1约定 2.2数据注释 2.3Fluent API 3.继承 3.1约定 4.支持字段 4.1约定 4.2数据注释 4.3Fluent API 4.3.1控制何时使用字段 4.3.2没有属性的字段 1.索引 索引是跨多个数据存储区的常见概念.尽管它们在数据存储中的实现可能会有所不同,但也可用于基于列(或一组列)更高效地进行查找. 1.1约定 按照约定,将在用作外键的每个属性(或一组属性)中创建索引. 1.2数

  • ASP.NET Core使用EF创建模型(必需和可选属性、最大长度、并发标记、阴影属性)

    目录 1.必需和可选属性 1.1约定 1.2数据批注 1.3Fluent API 2.最大长度 2.1约定 2.2数据批注 2.3Fluent API 3.并发标记 3.1约定 3.2数据注释 3.3Fluent API 4.时间戳/行版本 4.1约定 4.2数据注释 4.3Fluent API 5.阴影属性 5.1约定 5.2数据注释 5.3Fluent API 1.必需和可选属性 如果实体属性可以包含null,则将其视为可选.如果属性的有效值不可以包含null,则将其视为必需属性.映射到关系

  • ASP.NET Core使用EF创建模型

    目录 1.什么是Fluent API? 2.包含属性和排除属性 2.1包含属性 2.2排除属性 2.2.1数据批注 2.2.2Fluent API 3.主键 3.1数据批注 3.2Fluent API 4.生成值 4.1数据批注 4.1.1无值生成 4.1.2在添加时生成值 4.1.3在添加或更新时生成值 4.2Fluent API 4.2.1无值生成 4.2.2在添加时生成值 4.2.3在添加或更新时生成值 1.什么是Fluent API? EF中内嵌的约定将POCO类映射到表.但是,有时您无

  • ASP.NET Core使用EF为关系数据库建模

    目录 1.简介 2.表映射 2.1约定 2.2数据注释 2.3Fluent API 3.列映射 3.1约定 3.2数据注释 3.3Fluent API 4.数据类型 4.1约定 4.2数据注释 4.3Fluent API 5.主键 5.1约定 5.2数据注释 5.3Fluent API 6.默认架构 6.1约定 6.2数据注释 6.3Fluent API 7.默认值 7.1约定 7.2数据注释 7.3Fluent API 8.索引(关系数据库) 8.1约定 8.2数据注释 8.3Fluent A

  • ASP.NET Core使用EF创建关系模型

    目录 1.关系 2.术语定义 3.约定 4.完全定义的关系 5.无外键属性 6.单个导航属性 7.数据注释 7.1ForeignKey 7.2InverseProperty 8.Fluent API 8.1单个导航属性 8.2ForeignKey 8.3无导航属性 9.主体密钥 10.必需和可选的关系 11.级联删除 12.其他关系模式 12.1一对一 12.1.1数据注释 12.1.2Fluent API 12.2多对多 1.关系 关系定义两个实体之间的关系.在关系型数据库中,这由外键约束表示

随机推荐