实体框架核心现代数据访问教程(二)
七、数据库模式迁移
Entity Framework Core 包含一些工具,用于在应用开发或运行时从对象模型创建数据库,以及更改现有数据库的模式(在简单的情况下不会丢失数据)。
默认情况下,实体框架核心在启动时假设要寻址的数据库存在,并且处于正确的模式版本中。没有检查看看这是否真的是真的。例如,如果缺少表或列,或者不存在预期的关系,则在访问数据库中的对象时会出现运行时错误(例如,“无效的对象名' AircraftType '”)。
在运行时创建数据库
程序启动时,可以调用 context 类的Database子对象中的EnsureCreated()方法(见清单 7-1);如果不存在完整的数据库,这种方法将创建完整的数据库,并创建带有相关键和索引的表。
但是,如果数据库已经存在,EnsureCreated()就让它保持原样。然后,方法EnsureCreated()不检查数据库模式是否正确,即它是否对应于当前的对象模型。相反,EnsureCreated()使用以下命令:
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE') SELECT 1 ELSE SELECT 0
这将检查数据库中是否有任何表。如果没有表,则创建所有表。然而,只要数据库中有任何表,什么都不会发生,程序就会在运行时失败。下一节将描述模式迁移,您将获得更多的“智能”。
using DA;
using ITVisions;
using Microsoft.EntityFrameworkCore;
namespace EFC_Console
{
class CreateDatabaseAtRuntime
{
public static void Create()
{
CUI.MainHeadline("----------- Create Database at runtime");
using (var ctx = new WWWingsContext())
{
// GetDbConnection() requires using Microsoft.EntityFrameworkCore !
CUI.Print("Database: " + ctx.Database.GetDbConnection().ConnectionString);
var e = ctx.Database.EnsureCreated();
if (e)
{
CUI.Print("Database has been created");
}
else
{
CUI.Print("Database exists!");
}
}
}
}
}
Listing 7-1Using EnsureCreated()
开发时的模式迁移
在经典实体框架的 4.3 版本中,微软引入了模式迁移。这些模式迁移现在(略有不同)也可以在实体框架核心中使用。
模式迁移允许您执行以下操作:
- 以后更改数据库模式,同时保留现有数据
- 如有必要,取消更改
- 在开发时或应用启动时运行迁移
用于模式迁移的命令
与传统的实体框架一样,没有用于执行迁移的图形用户界面(GUI)。相反,您可以通过 Visual Studio 的包管理器控制台中的 PowerShell cmdlet 或外部命令行工具dotnet.exe(或其他操作系统上的dotnet)在命令行界面中执行所有操作。
要使用这些命令,请安装一个 NuGet 包。
Install-Package Microsoft.EntityFrameworkCore.Tools
不幸的是,这个包在项目中引入了许多新的程序集引用,这些引用在以后的运行时是不需要的。然而,由于 NuGet 包只在应用的启动项目中需要,而在其他项目中不需要,所以有一个简单的解决方案可以避免实际的启动项目膨胀。请遵循以下步骤:
- 创建一个新的控制台应用项目,比如使用名称
EFC_Tools。 - 安装实体框架核心工具包(
Microsoft.EntityFrameworkCore.Tools)。 - 从
EFC_Tools项目中,引用上下文类所在的项目。 - 将此
EFC_Tools项目作为启动项目。 - 在此执行所需的迁移命令。
- 再次更改启动项目。
如果您使用参数-startupproject指定启动项目在 cmdlets 中应该是什么,您可以避免更改启动项目。
Note
项目EFC_Tools不需要在以后部署给用户。
与传统的实体框架相比,微软已经改变了在实体框架核心中创建和使用模式迁移的过程的一些细节。开始时不需要运行Enable-Migrations命令;可以用Add-Migration直接启动项目。Command-Enable-Migrations命令仍然存在,但它只返回以下消息:“Enable-Migrations 已过时。使用Add-Migration开始使用迁移。”不调用Add-Migration的自动迁移在实体框架核心中不再可用。如前所述,使用Update-Database更新数据库。如果您更喜欢自己执行的 SQL 脚本,现在您将通过Script-Migration而不是Update-Database脚本接收它(参见图 7-1 )。
图 7-1
Flow of schema migration in the Entity Framework Core
ef.exe
在内部,PowerShell cmdlets 使用一个名为ef.exe(实体框架核心命令行工具)的经典命令行实用程序,它是 NuGet 包Microsoft.EntityFrameworkCore.Tools的一部分,位于Tools文件夹中。图 7-2 显示了该命令的帮助。
图 7-2
Help for ef.exe
添加-迁移
对于实体框架核心,您可以在 Visual Studio 中使用 PowerShell cmdlet Add-Migration启动模式迁移(甚至是第一次)。这是通过 NuGet 软件包管理器控制台(PMC)完成的。
- 实体框架核心工具实际上安装在当前的启动项目中。
- 该项目被选为上下文类所在的默认项目。
- 所有项目都可以在解决方案中编译。
与所有 PowerShell cmdlets 一样,cmdlet 名称的大小写无关。为了方便起见,每个项目中应该只有一个上下文类。否则,实体框架核心工具不知道指的是哪个上下文类(您将得到以下错误消息:“找到了多个 DbContext。指定要使用哪一个。对 PowerShell 命令使用'- context '参数,对 dotnet 命令使用'-Context '参数。).您必须通过为每个命令指定附加参数-Context来解决这个问题。
您必须指定一个可自由选择的名称,例如Add-Migration v1。在包管理器控制台中执行该命令会在上下文类的项目中创建一个包含三个文件和两个类的Migrations文件夹(参见图 7-3 )。
- 按照
Add-Migration中指定的名称创建一个类。这个类有两个文件,一个增加了.designer。这些文件的名称中还带有时间戳,表示迁移的创建时间。这个类继承自基类Microsoft.EntityFrameworkCore.Migrations.Migration。它在下文中被称为迁移类。 - 创建一个类,它采用上下文类的名称加上
ModelSnapshot,并继承自Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot。这个类在下文中被称为快照类。
迁移类有三个方法。Up()方法将数据库模式移动到它的新状态(如果没有先前的迁移,程序代码将在默认状态下创建数据库),而Down()方法撤销更改。方法BuildTargetModel()返回迁移创建时对象模型的状态。BuildTargetModel()使用从实体框架核心传递来的ModelBuilder的实例,就像 context 类中的OnModelCreating()方法一样。
在经典的实体框架中,微软将对象模型的当前状态存储在 XML 资源文件(.resx)中,当前状态的二进制表示在嵌入的 BLOB 中。然而,这种二进制表示不适合在源代码控制系统中进行比较,因此当多个开发人员创建模式迁移时会带来挑战。实体框架核心仍然可能是团队环境中冲突的来源,但这些冲突现在可以通过源代码控制系统更容易地解决,因为快照现在保存在 C#(或 Visual Basic)中。网)。
图 7-3
File created by Add-Migration
Snapshot类包含一个BuildModel()方法,该方法包含与第一次迁移中的BuildTargetModel()相同的程序代码。Snapshot类总是反映对象模型的最后状态,而BuildTargetModel()指的是迁移创建的时间。这两种方法的共同点是,它们用流畅的 API 语法表达整个对象模型,而不仅仅是OnModelCreating()的内容;他们还通过 Fluent API 制定约定和数据注释。这里可以看到 Fluent API 确实提供了实体框架核心的所有配置选项(见 www.n-tv.de/mediathek/videos/wirtschaft/Ryanair-will-Co-Piloten-abschaffen-article1428656.html )。
开发者可以自己扩展Up()和Down()方法,在这里执行自己的步骤。除了CreateTable()、DropTable()、AddColumn()、DropColumn()之外,还有CreateIndex()、AddPrimaryKey()、AddForeignKey()、DropTable()、DropIndex()、DropPrimaryKey()、DropForeignKey()、RenameColumn()、RenameTable()、MoveTable()、Sql()等操作。对于后一种操作,您可以执行任何 SQL 命令,例如,更新值或创建记录。在传统实体框架中用于填充数据库表的Seed()方法在实体框架核心中不存在。见图 7-4 。
图 7-4
Content of BuildModel() vs. BuildTargetModel() on first migration
Add-Migration不创建数据库,不读取数据库。Add-Migration根据当前快照类单独决定要做什么。因此,在 Entity Framework Core 中,您可以一个接一个地创建多个迁移,而实际上不必在中间更新数据库。
在经典的实体框架中,这是不同的。在这里,Add-Migration总是首先在数据库中查看它是否是最新的。否则,将出现错误“无法生成显式迁移,因为以下显式迁移处于待定状态”。不幸的是,这意味着如果不在其间更新您自己的数据库,您就不能一个接一个地创建多个模式迁移。尽管逐步创建模式迁移是明智的,但是您不希望每次都被迫更新数据库。
Attention
您可能会看到以下错误信息:“在程序集中找不到 DbContext。请确保您使用的是正确的程序集,并且该类型既不是抽象的也不是泛型的。这意味着您选择了错误的程序集来运行Add-Migration。这也可能意味着版本号中存在(小的)不一致。例如,如果在上下文类项目(EFC_DA)中使用了实体框架核心 2.0,但在EFC_Tools项目中安装了工具的 2.0.1 版本,则会出现此误导性错误消息。
清单 7-2 显示了迁移v2,它是在v1之后创建的,也是在我添加了属性Plz之后创建的,在类Persondetail中被遗忘了。Up()方法添加列AddColumn(),而Down()用DropColumn()清除它。
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace DA.Migrations
{
public partial class v2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Postcode",
table: "Persondetail",
maxLength: 8,
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Postcode",
table: "Persondetail");
}
}
}
Listing 7-2The Migration v2 Complements the Column Plz in the Table Persondetail
更新-数据库
然后,命令Update-Database会在您需要的任何时候将数据库带入迁移步骤所描述的状态。重要的是,此时,在上下文类的OnConfiguring()方法中,您通过UseSqlServer(ConnectionString). Update-Database将所需数据库的正确连接字符串传递给实体框架核心数据库提供者,它将在开发时实例化上下文类,OnConfiguring()将执行它。Update-Database将根据所有尚未执行的模式迁移的Up()方法创建数据库(如果还没有的话)以及所有的表。图 7-5 显示执行了两个模式迁移(v1和v2)。
图 7-5
Execution of Update-Database
Update-Database还在数据库中创建一个额外的__EFMigrationsHistory表,其中包含MigrationId和ProductVersion列。MigrationId对应的是没有文件扩展名的迁移类的文件名(如20171222143004_v1),ProductVersion是实体框架核心的版本号(如2.0.1-rtm-125)。在经典的实体框架中,该表被命名为__MigrationHistory,并且还包含一个对象模型状态的 BLOB。参见图 7-6 。
图 7-6
Content of the Table __EFMigrationsHistory
如果数据库已经存在,实体框架核心查看__EFMigrationsHistory表是否已经存在。如果有这个表并且记录了所有的迁移步骤,那么什么都不会发生。重复执行Update-Database不会产生错误(执行是幂等的)。实体框架核心不检查实际的数据库模式是否合适。因此,如果有人删除了一个表(通过 SQL Management Studio 或类似工具),只有当程序正在运行并且实体框架核心想要访问那个表时,问题才会出现。
如果__EFMigrationsHistory表不存在,Entity Framework Core 创建它,但同时它假设数据库模式还不存在,并执行所有的迁移步骤。但是,如果已经存在具有这些名称的表,Update-Database将失败(并显示错误消息“数据库中已经有一个名为‘xy’的对象。”).因此,如果有人删除了__EFMigrationsHistory表,因为他们认为它是多余的,这会破坏加载更多模式迁移的能力。
脚本迁移
虽然使用 Entity Framework Core 的正向工程足以执行 PowerShell commandlet Update-Database或等效的命令行命令dotnet ef database update来将模式更改直接导入开发系统,但是在分发应用时,您将需要其他机制。对于大多数公司来说,有必要使用一个 SQL 脚本,由数据库管理员(经过仔细检查)安装在屏蔽良好的数据库服务器上。这样的 SQL 脚本是通过Script-Migration命令行或者通过带有dotnet ef migrations script的命令行获得的。
Script-Migration创建带有迁移动作的 SQL 数据定义语言(DDL)脚本。Script-Migration不查看数据库,因此不知道其状态。cmdlet 总是为第一步之后的所有迁移步骤创建 SQL 脚本,而不进一步指定参数。如果您只想将单个迁移步骤作为 SQL 脚本,您必须用-from和-to来指定。这里有一个例子:
Script-Migration -from 20170905085855_v2 -to 20170905090511_v3
这个 cmdlet 内置了两个“困难”。
- 不能使用自赋名称(如
v2);您必须使用完整的迁移名称,包括由实体框架核心给出的时间戳。参数-from中的值 0 是初始状态的固定名称。 - 还执行参数
-from中指定的迁移步骤。因此,前面的命令并没有创建一个带有v2和v3差异的 SQL 脚本,而是创建了一个带有v1和v3差异的 SQL 脚本。
进一步的迁移步骤
即使在导入一个或多个迁移步骤后,您也可以随时创建其他迁移步骤。Update-Database检测迁移步骤是否尚未记录,然后执行。
迁移场景
对于哪种类型的模式改变,实体框架核心可以自动生成适当的模式迁移。添加表或列的模式更改并不重要。不管使用的字母顺序如何,列总是被添加到表的末尾(否则,整个表将不得不被删除并重新创建,这将要求数据被预先保存在临时表中)。
当您创建删除表或列的迁移步骤时,Add-Migration会用以下消息警告您:“操作被搭建,可能会导致数据丢失。请检查迁移的准确性。
有时,除了添加表和列之外,您还想做其他事情。例如,当重命名表或列时,您必须手动干预。这里,Add-Migration生成一些包含旧表或列删除的程序代码,并且创建一个新的表或列,因为在重命名时没有保留. NET 类或属性的特性。数据在迁移过程中丢失。现在,在这个迁移类中,你必须自己将一个DropTable()或一个CreateTable()转换成一个RenameTable(),以及将一个DropColumn()和一个CreateColumn()转换成一个RenameColumn()方法。
Note
从 Entity Framework Core 2.0 版开始,Entity Framework Core 将删除属性和添加相同数据类型和长度的属性视为重命名操作,因此在迁移类中创建了一个RenameColumn()方法。这可能是正确的;您可能希望删除一个列,然后创建一个新列。同样,您必须仔细检查生成的迁移类。
当更改数据类型时(例如,将一个nvarchar列从八个字符减少到五个字符),如果数据库中有更长的字符串,那么Migration会中止Update-Database(您会得到以下错误消息:“字符串或二进制数据将被截断。”).在这种情况下,您必须首先清理数据。例如,您可以通过添加以下内容将Postcode列从八个字符缩短为五个字符:
migrationBuilder.Sql("update Persondetail set Postcode = left(Postcode, 5)")
到Up()方法之前,执行这个:
migrationBuilder.AlterColumn<string>(name: "Postcode", table: "Persondetail", maxLength: 5, nullable: true).
改变基数的模式迁移是困难的。例如,假设您必须从Passenger和Persondetail之间的 1:0/1 关系中突然建立一个 1:N 关系,因为需求已经改变,所以每个Passenger现在可能有多个地址。随着基数的变化,实体框架核心不再能够保持一致的数据状态。虽然之前在Passenger表中有一个DetailID列引用了Persondetail表中的一个记录,但是在Persondetail中的模式构造之后必须有一个PersonalID列引用Passenger。尽管实体框架核心删除了一列并创建了另一个新列,但它不会用适当的值填充新列。这里,您必须使用迁移类中的Sql()方法手动复制这些值。
不幸的是,实体框架核心工具也会生成迁移代码,首先删除DetailID列,然后在PersonDetails表中重新创建PersonID。当然,在这里获取数据是行不通的。清单 7-3 显示了不同顺序的正确解决方案,并使用Sql()方法复制密钥。
namespace EFC_DA.Migrations
{
public partial class Kardinaliaet11wird1N : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// First create a new column on the N-side
migrationBuilder.AddColumn<int>(
name: "PassengerPersonID",
table: "Persondetail",
nullable: true);
// Now copy the values from the 1-side
migrationBuilder.Sql("update Persondetail set PassengerPersonID = Passenger.PersonID FROM Passenger INNER JOIN Persondetail ON Passenger.DetailID = Persondetail.ID");
// Then delete the column on the 1-side first
migrationBuilder.DropForeignKey(
name: "FK_Passenger_Persondetail_DetailID",
table: "Passenger");
migrationBuilder.DropIndex(
name: "IX_Passenger_DetailID",
table: "Passenger");
migrationBuilder.DropColumn(
name: "DetailID",
table: "Passenger");
// Then create index and FK for new column
migrationBuilder.CreateIndex(
name: "IX_Persondetail_PassengerPersonID",
table: "Persondetail",
column: "PassengerPersonID");
migrationBuilder.AddForeignKey(
name: "FK_Persondetail_Passenger_PassengerPersonID",
table: "Persondetail",
column: "PassengerPersonID",
principalTable: "Passenger",
principalColumn: "PersonID",
onDelete: ReferentialAction.Restrict);
}
}
Listing 7-3Up() Migration Class Method for a Cardinality Change from 1:0/1 to 1:N
更多选项
使用Update-Database,您还可以返回到数据库模式的先前状态。例如,在使用以下命令导入版本 3 后,您可以返回到版本 2:
Update-Database-Migration v2
Update-Database使用迁移类的Down()方法。使用Script-Migration,你还可以为“倒下”的情况创建一个脚本。这里有一个例子:
Script-Migration -from v3 -to v2
Remove-Migration允许您为最近的迁移步骤从 Visual Studio 中移除迁移类。
Important
您不应手动删除迁移类,因为快照类将不再是最新的。因此,下次创建迁移时,手动删除的迁移步骤将被忽略。如果手动删除迁移类,还必须手动调整快照类。
Remove-Migration检查数据库中是否已经应用了最后一个迁移步骤。如果是这样,将不会删除迁移类和更改快照类。错误消息如下:“迁移已应用于数据库。取消应用并重试。如果迁移已应用于其他数据库,请考虑使用新的迁移来恢复其更改。您可以通过参数-force绕过该检查。如果不在数据库模式中进行手动干预,您可能无法再在数据库中创建新的迁移步骤,因为这些步骤可能会尝试重新创建以前创建的表或列。
Add-Migration、Remove-Migration、Update-Database和Script-Migration各有三个公共参数,在此列出:
-StartupProject:如果不想更改启动项目,则设置包含实体框架核心工具包的 Visual Studio 项目-Project:指定上下文类所在的 Visual Studio 项目-Context:如果 Visual Studio 项目中有多个上下文类,则设置上下文类(带有命名空间)
这里有一个例子:
Update-Database v2 -StartupProject EFC_Tools -Project EFC_DA -Context WWWingsContext
为了避免在 cmdlet 中重复使用这些参数,您可以使用 cmdlet Use-DbContext设置这些值,从而确保所有后续的 cmdlet 调用都使用这些值。
Use-dbContext -StartupProject EFC_Tools -Project EFC_DA -context WWWingsContext
与 TFS 相关的模式迁移问题
在与 Team Foundation Server (TFS)的版本管理系统相结合的情况下(至少在具有服务器工作区的经典版本中,它对文件进行写保护),实体框架核心的工具存在困难。它报告无法修改迁移文件夹中的文件。您将得到以下错误:“对路径…wwwingscontextmodelnsnapshot . cs 的访问被拒绝。在这种情况下,在运行该命令之前,您应该通过签出以进行编辑来解锁迁移文件夹。
另一个问题是Remove-Migration会删除磁盘上的文件,但不会从 TFS 版本中删除。您必须在“挂起的更改”窗口中手动选择 Visual Studio 命令“撤消”。
运行时模式迁移
在极少数情况下,会发布一个小应用(例如,控制台应用),将模式更改导入目标数据库。这些案例包括以下内容:
- 为没有经验的数据库管理员或不熟悉 SQL 的客户服务代表提供工具
- 作为发布管道中自动化集成测试的一部分,安装或更新数据库
- 在最终用户系统上安装或更新本地数据库(如果是移动应用,应该在启动时将模式迁移直接安装到实际的应用中)
对于适合程序执行模式迁移的情况,实体框架核心提供了方法ctx.Database.GetMigrations()、ctx.Database.GetAppliedMigrations()和ctx.Database.Migrate()。这些方法可以使开发人员不必编写工具来确定哪些模式迁移正在进行,然后注入适当的 SQL 脚本。
与传统的实体框架不同,在应用的第一次数据库访问期间,实体框架核心不检查模式是否是最新的。程序可能会在出现错误时运行(例如,“无效的列名‘邮政编码’”)。通过调用 context 类的Database对象中的方法Migrate(),您可以在启动时确保数据库模式是最新的(参见清单 7-4 )。Migrate()可能会执行缺失的迁移步骤,这是可能的,因为迁移类是项目编译的一部分,项目包含上下文类。
Note
对于运行时的模式迁移,不需要 NuGet 包Microsoft.EntityFrameworkCore.Tools。
using (var ctx = new WWWingsContext())
{
ctx.Database.Migrate();
}
Listing 7-4Running a Schema Migration at Runtime Using the Migrate() Method
禁止同时使用EnsureCreated()和Migrate()。甚至方法Migrate()的工具提示也警告不要这么做。如果您仍然想尝试,您将得到一个不起眼的运行时错误“已经添加了一个具有相同键的项目。”
Note
如果模式已经处于必要的状态,那么Migrate()的启动成本非常低。架构迁移可能需要几秒钟时间;但是,如果没有迁移,您的代码可能会失败。
八、使用 LINQ 读取数据
与经典的实体框架一样,实体框架核心允许您使用语言集成查询(LINQ)编写数据库查询。
LINQ 是 2007 年在中引入的不同数据存储的通用查询语言。NET 框架 3.5;它也存在于。NET Core 以及 Mono 和 Xamarin。微软从一开始就在经典的实体框架中使用 LINQ,它被实现为实体的 LINQ。微软在实体框架核心中不再使用这个术语;它就叫 LINQ。经典实体框架和实体框架核心在 LINQ 执行方面存在一些积极和消极的差异。
上下文类
Entity Framework Core 中所有 LINQ 查询的起点是在对现有数据库进行逆向工程时创建的上下文类,或者在进行正向工程时手动创建的上下文类。实体框架核心中的上下文类总是从基类Microsoft.EntityFrameworkCore.DbContext继承而来。经典实体框架中存在的ObjectContext的替代基类已从实体框架核心中删除。因此,你必须使用DbContext进行所有的 LINQ 操作。但是即使是基类DbContext在实体框架核心也有一点改变。
DbContext类实现了IDisposable接口。作为Dispose()方法的一部分,DbContext释放所有分配的资源,包括对所有加载了变更跟踪的对象的引用。
Tip
因此,一旦工作完成,上下文类用户总是调用Dispose()是很重要的。最好用一个using(){ ... }挡!
LINQ 询问
实例化上下文类后,您可以制定一个 LINQ 查询。该查询不一定立即执行;它最初是以带有接口IQueryable<T>的对象的形式出现的。在所谓的延迟执行的意义上,当结果被实际使用时(例如,在一个foreach循环中)或者当结果被转换成另一个集合类型时,LINQ 查询被执行。您可以使用 LINQ 转换运算符强制执行查询,换句话说,ToList()、ToArray()、ToLookup()、ToDictionary()、Single()、SingleOrDefault()、First()、FirstOrDefault(),或者使用聚合运算符,如Count()、Min()、Max()或Sum()。
因为IQueryable<T>是IEnumerable<T>的一个子类型,你可以用IQueryable<T>在一个对象上开始一个foreach循环。这将导致查询立即运行。此外,实体框架核心保持数据库连接打开,直到获取最后一个对象,这可能导致不必要的副作用。因此,在使用 RAM 中的数据之前,您应该总是显式地使用前面的转换或聚合操作符之一,因为在这种情况下,实体框架核心将关闭数据库连接。但是,因为实体框架核心是基于 ADO.NET 的,所以数据库连接实际上并没有立即关闭,而是返回到 ADO.NET 连接池。同样,数据绑定到接口为IQueryable<T>的对象会触发数据检索。
图 8-1 显示了 LINQ 查询的内部处理。首先,LINQ 查询被转换成表达式树。表达式树创建一个 SQL 命令,实体框架核心使用来自 ADO.NET 的Command对象将该命令发送到数据库管理系统。实体框架核心为 SQL 命令提供了一个缓存,以防止将 LINQ 转换为 SQL 的开销增加到相同命令的两次。
图 8-1
Internals for running a LINQ command through Entity Framework Core
数据库管理系统分析该查询,并检查是否已经有合适的执行计划。如果缓存中不存在,将会创建它。此后,数据库管理系统执行查询并将结果集传递给实体框架核心。然后实体框架核心使用一个DataReader对象读取结果集,但是用户代码看不到它,因为实体框架核心将DataReader行具体化为对象。除了在非跟踪模式下,实体框架核心查看要物化的对象是否已经在实体框架核心上下文的一级缓存中。如果对象在那里,物化就被消除了。但是,这也意味着如果一个对象在 RAM 中,用户将不会从数据库中获得记录的当前状态,而是从缓存中获得对象,尽管重新执行了一个 SQL 命令。
清单 8-1 显示了一个简单的 LINQ 查询,它返回从一个出发地点出发的所有未预订的航班,按日期和出发地点排序。ToList()将IQueryable<Flight>变成带有接口IEnumerable<T>的List<Flight>。然而,在实践中,程序代码中经常使用关键字var,而不是具体的类型名。
public static void LINQ_List()
{
var city = "Berlin";
// Instantiate context
using (var ctx = new WWWingsContext())
{
// Define queries, but do not execute yet
IQueryable<Flight> query = (from x in ctx.FlightSet
where x.Departure == city &&
x.FreeSeats > 0
orderby x.Date, x.Departure
select x);
// Run query now
List<Flight> flightSet = query.ToList();
// Count loaded objects
var count = flightSet.Count;
Console.WriteLine("Number of loaded flights: " + count);
// Print results
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
} // End using-Block -> Dispose() will be called
Listing 8-1LINQ Query That Returns All Nonbooked Flights from a Departure Location
在 LINQ 的方法语法中可能有一个更简洁的公式,如下所示:
var query2 = ctx.FlightSet.Where(x => x.Departure == city && x.FreeSeats > 0)
.OrderBy(x => x.Date).ThenBy(x => x.Departure);
当然,也可以将对ToList()的调用与 LINQ 查询的定义结合起来,从而立即执行 LINQ 查询。
var flightSet2 = (from x in ctx.FlightSet
where x.Departure == city &&
x.FreeSeats > 0
orderby x.Date, x.Departure
select x).ToList();
但是,拆分表示法的优点是,您可以在执行查询之前附加更多的操作。
以下是执行的 SQL 查询:
SELECT [x].[FlightNo], [x].[AircraftTypeID], [x].[AirlineCode], [x].[CopilotId], [x].[FlightDate], [x].[Departure], [x].[Destination], [x].[FreeSeats], [x].[LastChange], [x].[Memo], [x].[NonSmokingFlight], [x].[PilotId], [x].[Price], [x].[Seats], [x].[Strikebound], [x].[Timestamp], [x].[Utilization]
FROM [Flight] AS [x]
WHERE ([x].[Departure] = @__city_0) AND ([x].[FreeSeats] > 0)
ORDER BY [x].[FlightDate], [x].[Departure]
Note
理论上,你可以调用方法query.Count()而不是属性flightSet.Count。但是,这会产生一个新的数据库查询,提供记录的数量。这是多余的,因为对象已经物化,可以在 RAM 中快速计数。只有当您想确定数据库中的记录数量是否已经改变时,使用query.Count()访问 DBMS 才有意义。
LINQ 查询的逐步组合
清单 8-2 展示了如果值的变量不包含零或空字符串,如何根据具体情况将出发或目的地的附加条件附加到基本查询FreeSeats > 0上。这是用户设置过滤器的典型情况。如果用户没有在筛选字段中输入任何内容,那么他们不希望看到值为空的记录,并且希望在查询过程中忽略该筛选。
public static void LINQ_Composition()
{
var departure = "";
var destination = "Rome";
// Create context instance
using (var ctx = new WWWingsContext())
{
// Define query, but do not execute yet
IQueryable<Flight> query = from x in ctx.FlightSet
where x.FreeSeats > 0
select x;
// Conditional addition of further conditions
if (!String.IsNullOrEmpty(departure)) query = query.Where(x => x.Departure == departure);
if (!String.IsNullOrEmpty(destination)) query = query.Where(x => x.Destination == destination);
// now use sorting, otherwise there will be problems with variable query type (IQueryable<Flight> vs. IOrderedQueryable<Flight>)
var querySorted = from x in query // IOrderedQueryable<Flight>
orderby x.Date, x.Departure
select x;
// Execute query now
List<Flight> flightSet = querySorted.ToList();
// Count loaded objects
long c = flightSet.Count;
Console.WriteLine("Number of loaded flights: " + c);
// Print result
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
} // End using-Block -> Dispose()
Listing 8-2LINQ Query That Returns All Unbooked Flights on a Route, with Both Departure and Destination Optional
var 的使用
在实践中,当使用 LINQ 时,使用关键字var,而不是特定的类型名,如IQueryable<Flight>。关于关键字var(特别是在 Visual Basic 中使用不带类型的Dim时),开发人员之间仍然有很多争论。网)。在 LINQ 的配合下,var经常简化编码。对于 LINQ,使用一些操作符如orderby会改变返回类型。没有orderby,你得到一个实现IQueryable<Flight>的对象。有了orderby,就是一个IOrderedQueryable<Flight>。因此,在更改 LINQ 查询时,您经常需要更改变量类型。当使用关键字var时,这是不必要的。
知识库模式
除了在小型应用中,您不应该将数据访问代码保存在用户界面中。已经建立了存储库模式来封装一个或多个(连接的)表的数据访问代码。repository 类提供返回单个对象或对象集的方法,或者返回允许您插入、删除和修改记录的方法。
一个IQueryable<T>也可以用作方法的返回值,这样方法的调用者也可以扩展查询。但是,只有当上下文实例在方法结束后仍然存在,从而可以在以后执行查询时,这才有意义。因此,您必须将上下文实例作为类的一个属性,并为以后调用Dispose()时上下文实例的销毁提供IDisposable接口(参见清单 8-3 和存储库中的类FlightManager)。然后调用者可以扩展查询,并且应该使用带有using()块的类FlightManager来确保对Dispose()的调用。参见清单 8-4 。
Note
您可以在本书的附录 A 中看到运行中的存储库模式。在这里,您还将看到如何为所有存储库类使用一个公共基类,从而减少存储库类中的代码。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using BO;
using DA;
namespace BL
{
/// <summary>
/// Repository class for Flight entities
/// </summary>
public class FlightManager : IDisposable
{
public FlightManager()
{
// create instance of context when FlightManager is created
ctx = new WWWingsContext();
}
// keep one EFCore context per instance
private WWWingsContext ctx;
/// <summary>
/// Dispose context if FlightManager is disposed
/// </summary>
public void Dispose() { ctx.Dispose(); }
/// <summary>
/// Get one flight
/// </summary>
public Flight GetFlight(int flightID)
{
return ctx.FlightSet.Find(flightID);
}
/// <summary>
/// Get all flights on a route
/// </summary>
public List<Flight> GetFlightSet(string departure, string destination)
{
var query = GetAllAvailableFlightsInTheFuture();
if (!String.IsNullOrEmpty(departure)) query = from f in query
where f.Departure == departure
select f;
if (!String.IsNullOrEmpty(destination)) query = query.Where(f => f.Destination == destination);
List<Flight> result = query.ToList();
return result;
}
/// <summary>
/// Base query that callre can extend
/// </summary>
public IQueryable<Flight> GetAllAvailableFlightsInTheFuture()
{
var now = DateTime.Now;
var query = (from x in ctx.FlightSet
where x.FreeSeats > 0 && x.Date > now
select x);
return query;
}
/// <summary>
/// Get the combined list of all departures and all destinations
/// </summary>
/// <returns></returns>
public List<string> GetAirports()
{
var l1 = ctx.FlightSet.Select(f => f.Departure).Distinct();
var l2 = ctx.FlightSet.Select(f => f.Destination).Distinct();
var l3 = l1.Union(l2).Distinct();
return l3.OrderBy(z => z).ToList();
}
/// <summary>
/// Delegate SaveChanges() to the context class
/// </summary>
/// <returns></returns>
public int Save()
{
return ctx.SaveChanges();
}
/// <summary>
/// This overload checks if there are objects in the list that do not belong to the context. These are inserted with Add().
/// </summary>
public int Save(List<Flight> flightSet)
{
foreach (Flight f in flightSet)
{
if (ctx.Entry(f).State == EntityState.Detached)
{
ctx.FlightSet.Add(f);
}
}
return Save();
}
/// <summary>
/// Remove flight (Delegated to context class)
/// </summary>
/// <param name="f"></param>
public void RemoveFlight(Flight f)
{
ctx.Remove(f);
}
/// <summary>
/// Add flight (Delegated to context class)
/// </summary>
/// <param name="f"></param>
public void Add(Flight f)
{
ctx.Add(f);
}
/// <summary>
/// Reduces the number of free seats on the flight, if seats are still available. Returns true if successful, false otherwise.
/// </summary>
/// <param name="flightID"></param>
/// <param name="numberOfSeats"></param>
/// <returns>true, wenn erfolgreich</returns>
public bool ReducePlatzAnzahl(int flightID, short numberOfSeats)
{
var f = GetFlight(flightID);
if (f != null)
{
if (f.FreeSeats >= numberOfSeats)
{
f.FreeSeats -= numberOfSeats;
ctx.SaveChanges();
return true;
}
}
return false;
}
}
}
Listing 8-3Repository Class
That Returns an IQuerable <Flight>
public static void LINQ_RepositoryPattern()
{
using (var fm = new BL.FlightManager())
{
IQueryable<Flight> query = fm.GetAllAvailableFlightsInTheFuture();
// Extend base query now
query = query.Where(f => f.Departure == "Berlin");
// Execute the query now
var flightSet = query.ToList();
Console.WriteLine("Number of loaded flights: " + flightSet.Count);
}
}
Listing 8-4Using the Repository Class from Listing 8-3
LINQ 分页查询
分页意味着从结果集中只能传递一个特定范围的记录。这可以在 LINQ 用方法Skip()和Take()(或者 Visual Basic 中的语言元素Skip和Take实现。网)。
清单 8-5 显示了一个更复杂的 LINQ 查询。它将搜索比赛
- 至少有一个空座位
- 至少有一个预订
- 有一个乘客叫穆勒
- 飞行员出生于 1972 年 1 月 1 日之前
- 有一个副驾驶
然后,通过在数据库管理系统中进行分页,从结果集中跳过前 50 个数据记录,并且仅传递后面的 10 个数据记录(即,数据记录 51 至 60)。
[EFCBook("Paging")]
public static void LINQ_QueryWithPaging()
{
CUI.MainHeadline(nameof(LINQ_QueryWithPaging));
string name = "Müller";
DateTime date = new DateTime(1972, 1, 1);
// Create context instance
using (var ctx = new WWWingsContext())
{
// Define query and execute
var flightSet = (from f in ctx.FlightSet
where f.FreeSeats > 0 &&
f.BookingSet.Count > 0 &&
f.BookingSet.Any(b => b.Passenger.Surname == name) &&
f.Pilot.Birthday < date &&
f.Copilot != null
select f).Skip(5).Take(10).ToList();
// Count number of loaded objects
var c = flightSet.Count;
Console.WriteLine("Number of found flights: " + c);
// Print objects
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
} // End using-Block -> Dispose()
}
Listing 8-5Complex LINQ Query
下面的 SQL 命令是清单 8-5 中发送的 SQL 命令,它比 LINQ 的对应命令复杂得多。此命令已发送到 Microsoft SQL Server 2017,并使用 SQL Server 附带的 SQL Server Profiler 工具进行了检索。SQL Server 版本在这里实际上很重要;2008 SQL ANSI 标准(http:// / www.iso.org/iso/home/store/catalogue_tc/catalogue_tc_browse.htm?commid=45342 )中带有关键字OFFSET、FETCH FIRST和FETCH NEXT的行限制子句从 2012 年版本开始就受到 Microsoft SQL Server 的支持。Oracle 从版本 1.7.2013 年 7 月 1 日发布)开始提供这种支持。对于不支持这种新语法的 DBMSs,Entity Framework Core 需要用rownumber()函数创建一个更加复杂的查询,并选择实现Skip()。
Note
实体框架核心的一个很好的改进是在 SQL 命令中使用了 LINQ 查询中的变量名(这里是f和b)。在经典的实体框架中,使用了诸如extend1、extend2、extend3等名称。如果 SQL 中的 Entity Framework Core 多次需要一个表的别名,ORM 会在变量名后面附加一个数字(参见下面 SQL 代码中的[b0])。
exec sp_executesql N'SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
INNER JOIN [Employee] AS [f.Pilot] ON [f].[PilotId] = [f.Pilot].[PersonID]
WHERE ([f.Pilot].[Discriminator] = N''Pilot'') AND ((((([f].[FreeSeats] > 0) AND ((
SELECT COUNT(*)
FROM [Booking] AS [b]
WHERE [f].[FlightNo] = [b].[FlightNo]
) > 0)) AND EXISTS (
SELECT 1
FROM [Booking] AS [b0]
INNER JOIN [Passenger] AS [b.Passenger] ON [b0].[PassengerID] = [b.Passenger].[PersonID]
WHERE ([b.Passenger].[Surname] = @__name_0) AND ([f].[FlightNo] = [b0].[FlightNo]))) AND ([f.Pilot].[Birthday] < @__date_1)) AND [f].[CopilotId] IS NOT NULL)
ORDER BY (SELECT 1)
OFFSET @__p_2 ROWS FETCH NEXT @__p_3 ROWS ONLY',N'@__name_0 nvarchar(4000),@__date_1 datetime2(7),@__p_2 int,@__p_3 int',@__name_0=N'Müller',@__date_1='1972-01-01 00:00:00',@__p_2=5,@__p_3=10
预测
在关系数据库中,对所选列的限制称为投影(参见 https://en.wikipedia.org/wiki/Set_theory )。如果不是所有的列都是真正需要的,那么装载一个表的所有列通常是一个严重的性能错误。
到实体类型的投影
到目前为止显示的 LINQ 查询总是实际加载和具体化Flight表的所有列。清单 8-6 显示了一个带有select new Flight()和所需列的投影。在执行了ToList()方法之后,您会收到一个包含所有属性的Flight对象的列表(因为类是这样定义的),但是只填充了指定的属性。
public static void Projection_Read()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline(nameof(Projection_Read));
var query = from f in ctx.FlightSet
where f.FlightNo > 100
orderby f.FlightNo
select new Flight()
{
FlightNo = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats
};
var flightSet = query.ToList();
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
}
}
Listing 8-6LINQ Query with Projection
清单 8-6 的以下 SQL 输出证明了实体框架核心确实只请求数据库管理系统中所需的列:
SELECT [f].[FlightNo], [f].[FlightDate] AS [Date], [f].[Departure], [f].[Destination], [f].[FreeSeats]
FROM [Flight] AS [f]
WHERE [f].[FlightNo] > 100
ORDER BY [f].[FlightNo]
Note
对实体类的直接支持是 Entity Framework Core 相对于传统实体框架的一个主要优势。在经典的实体框架中,对于实体类和复杂类型,投影是不可能的;只有匿名类型和非实体类可以用于投影。由于匿名类型的限制(实例是只读的,不能在方法中作为返回值使用),通常需要复制实体类的实例。为此,您可以使用 NuGet 包自动映射器。EF6 ( https://github.com/AutoMapper/AutoMapper.EF6 )用扩展法ProjectTo<T>()。
匿名类型的投影
到匿名类型的投影是可能的。在这种情况下,不应该在new操作符后指定类名。如果属性的名字不变,那么在初始化块中,只需要简单的提一下属性,而不是赋值{Departure = f.Departure, ...},如下:{f.Departure, f.Destination, ...}。
Note
匿名类型对于实体框架核心是未知的。如果您尝试用ctx.Entry(f).State查询匿名状态或调用ctx.Attach(f),您会得到以下运行时错误:“找不到实体类型'< > f__AnonymousType8 ,byte[ ] '”。请确保该实体类型已添加到模型中。
f.FreeSeats--
第二十章讲述了如何使用对象到对象的映射将匿名类型映射到其他类型。清单 8-7 显示了匿名类型的投影。
public static void Projection_AnonymousType()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline(nameof(Projection_AnonymousType));
var q = (from f in ctx.FlightSet
where f.FlightNo > 100
orderby f.FlightNo
select new
{
FlightID = f.FlightNo,
f.Date,
f.Departure,
f.Destination,
f.FreeSeats,
f.Timestamp
}).Take(2);
var flightSet = q.ToList();
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightID} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
Console.WriteLine("Number of flights: " + flightSet.Count);
foreach (var f in flightSet)
{
Console.WriteLine(f.FlightID);
// not posssible: Console.WriteLine("Before attach: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
// not posssible: ctx.Attach(f);
// not posssible: Console.WriteLine("After attach: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
// not posssible:
// f.FreeSeats--;
// not posssible: Console.WriteLine("After Änderung: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
var count = ctx.SaveChanges(); // no changes can be saved
Console.WriteLine("Number of saved changes: " + count);
// not posssible: Console.WriteLine("After saving: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
}
}
}
Listing 8-7Projection to an Anonymous Type
任意类型的投影
投影的目标也可以是任何其他类,根据软件架构的结构,这些类将被称为业务对象(BO)或数据传输对象(DTO)。与实体类投影一样,您必须在new之后提到类名,并且完整的赋值对于初始化是必要的,如下:{Departure = f.Departure, ...}。清单 8-8 显示了到 DTO 的投影。
Note
与匿名类型一样,在这种情况下,实体框架核心不知道该类。因此,使用Attach()请求状态并保存更改是不可能的。
class FlightDTO
{
public int FlightID { get; set; }
public DateTime Date { get; set; }
public string Departure { get; set; }
public string Destination { get; set; }
public short? FreeSeats { get; set; }
public byte[] Timestamp { get; set; }
}
public static void Projection_DTO()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline(nameof(Projection_DTO));
var q = (from f in ctx.FlightSet
where f.FlightNo > 100
orderby f.FlightNo
select new FlightDTO()
{
FlightID = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats,
Timestamp = f.Timestamp
}).Take(2);
var flightSet = q.ToList();
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightID} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
Console.WriteLine("Number of flights: " + flightSet.Count);
foreach (var f in flightSet)
{
Console.WriteLine(f.FlightID);
// not posssible: Console.WriteLine("Before attach: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
// not posssible: ctx.Attach(f);
// not posssible: Console.WriteLine("After attach: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
// not posssible:
// f.FreeSeats--;
// not posssible: Console.WriteLine("After Änderung: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
var anz = ctx.SaveChanges(); // no changes can be saved
Console.WriteLine("Number of saved changes: " + anz);
// not posssible: Console.WriteLine("After saving: " + f + " State: " + ctx.Entry(f).State + " Timestamp: " + ByteArrayToString(f.Timestamp));
}
}
}
Listing 8-8Projection to a DTO
查询单个对象
LINQ 提供了四种操作来选择集合中的第一个或唯一元素,如下所示:
First():一个集合中的第一个元素。如果集合中有多个元素,则除了第一个元素之外,其他元素都将被丢弃。如果没有元素,就会发生运行时错误。FirstOrDefault():当金额为空时,集合的第一个元素或默认值(对于引用类型null或Nothing)。如果集合中有多个元素,则除了第一个元素之外,其他元素都将被丢弃。Single():一个集合中唯一的元素。如果集合中没有元素或有多个元素,则会发生运行时错误。SingleOrDefault():一个集合中唯一的元素。如果没有元素,则返回默认值(对于引用类型null或Nothing)。如果集合中有多个项目,则会发生运行时错误。
First()和FirstOrDefault()使用 SQL 操作符TOP(1). Single()限制数据库端的输出数量,SingleOrDefault()使用TOP(2)确定是否有多个元素,这会导致运行时错误。清单 8-9 显示了 LINQ 查询的代码。
public static void LINQ_SingleOrDefault()
{
using (var ctx = new WWWingsContext())
{
var FlightNr = 101;
var f = (from x in ctx.FlightSet
where x.FlightNo == FlightNr
select x).SingleOrDefault();
if (f != null)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
else
{
Console.WriteLine("Flight not found!");
}
} // End using-Block -> Dispose()
}
Listing 8-9LINQ Query for a Single Object with SingleOrDefault()
使用带有 Find()的主键进行加载
经典实体框架中的DbSet<T>类提供了一个Find()方法,作为使用主键和 LINQ 加载对象的替代方法。Find()传递主键的值。如果有多部分主键,也可以传递几个值,比如Find ("Holger", "Schwichtenberg", 12345)如果主键由两个字符串和一个数字组成。Find()在实体框架核心版本 1.0 中不可用,但已集成到版本 1.1 中。清单 8-10 显示了 LINQ 查询。
Note
Find()具有特殊的行为,首先在实体框架核心上下文的一级缓存中查找对象,只有在对象不在那里时才启动数据库查询。方法Single()、SingleOrDefault()、First()和FirstOrDefault()总是询问数据库,即使对象存在于本地缓存中!
public static void LINQ_Find()
{
CUI.MainHeadline(nameof(LINQ_Find));
using (var ctx = new WWWingsContext())
{
ctx.FlightSet.ToList(); // Caching all flights in context (here as an example only to show the caching effect!)
var FlightNr = 101;
var f = ctx.FlightSet.Find(FlightNr); // Flight is loaded from cache!
if (f != null)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
else
{
Console.WriteLine("Flight not found!");
}
} // End using-Block -> Dispose()
}
Listing 8-10LINQ Query for a Single Object with Find
在 RAM 中而不是在数据库中使用 LINQ(客户端评估)
清单 8-11 显示了一个使用 LINQ 分组操作符(group by或GroupBy())进行分组的 LINQ 查询。这个查询提供了想要的结果(每次出发的航班数量),但是对于大量的数据,这个查询需要花费很多时间。在研究原因的时候,你会发现在发给数据库管理系统的 SQL 命令中,分组是完全缺失的。实体框架核心已经加载了所有记录,并在 RAM 中进行分组,这是不好的,也是意外的。
Attention
事实上,Entity Framework Core 版本 1.x 和 2.0 不支持将 LINQ 分组转换成 SQL 的GROUP BY语法,这是 Entity Framework Core 中一个可怕的缺陷(参见“解决 GroupBy 问题”一节)。计划对实体框架核心 2.1 版进行改进;参见附录 C 。
using (var ctx = new WWWingsContext())
{
Console.WriteLine(ctx.Database.GetType().FullName);
ctx.Log();
var groups = (from p in ctx.FlightSet
group p by p.Departure into g
select new { City = g.Key, Count = g.Count() }).Where(x => x.Count > 5).OrderBy(x => x.Count);
// First roundtrip to the database (done intentionally here!)
var count = groups.Count();
Console.WriteLine("Number of groups: " + count);
if (count == 0) return;
// Second roundtrip to the database
foreach (var g in groups.ToList())
{
Console.WriteLine(g.City + ": " + g.Count);
}
}
Listing 8-11Determine the Number of Flights per Departure
清单 8-11 显示了发送到数据库管理系统的 SQL 命令(两次:一次用于Count(),一次用于ToList())。
SELECT [p0].[FlightNo],
[p0].[AircraftTypeID],
[p0].[AirlineCode],
[p0].[CopilotId],
[p0].[FlightDate],
[p0].[Departure],
[p0].[Destination],
[p0].[FreeSeats],
[p0].[LastChange],
[p0].[Memo],
[p0].[NonSmokingFlight],
[p0].[PilotId],
[p0].[Price],
[p0].[Seats],
[p0].[Strikebound],
[p0].[Timestamp],
[p0].[Utilization]
FROM [Flight] AS [p0]
ORDER BY [p0].[Departure]
不幸的是,在许多其他情况下,实体框架核心在 RAM 中而不是在数据库中执行操作。
对于下面的查询,在 Entity Framework Core 1.x 中,只有通过FlightNo的过滤发生在数据库中。
var q2 = from f to ctx.FlightSet
where f.FlightNo > 100
&& f.FreeSeats.ToString().Contains("1")
orderby f.FlightNo
select f;
ToString().Contains()无法被翻译并在 RAM 中执行该条件。在 2.0 版中,整个 LINQ 命令都被翻译成 SQL。
对于以下查询,AddDays()在 Entity Framework Core 1.x 中无法翻译,因此在数据库管理系统中只对空闲座位进行了过滤,而没有进行日期过滤。
var q3 = from f to ctx.FlightSet
where f.FreeSeats> 0 &&
f.Date > DateTime.Now.AddDays (10)
orderby f.FlightNo
select f;
在实体框架核心 2.0 中也修复了这一点。
不幸的是,在 Entity Framework Core 2.0 的 RAM 中也出现了以下带有 LINQ 运算符Union()的查询:
var all places = (from f in ctx.FlightSet select f.Departure.Union(from f in ctx.FlightSet select f.Destination).Count();
尽管这里只需要一个数字,但实体框架核心将执行以下操作:
SELECT [f]. [Departure]
FROM [Flight] AS [f]
SELECT [f0]. [Destination]
FROM [Flight] AS [f0]
Note
应该提到的是,以前的一些 LINQ 查询在经典的实体框架中根本不可执行。它们已被编译,但导致了运行时错误。实体框架核心中的新解决方案是否是更好的解决方案是有争议的。虽然订单现在是可能的,但潜伏着一个大陷阱。毕竟微软已经在路线图( https://github.com/aspnet/EntityFrameworkCore/wiki/Roadmap )中宣布,在未来版本的 Entity Framework Core 中你可以在数据库管理系统中执行更多的操作。
当在 RAM 中运行时,由于加载了太多的记录,可能会出现严重的性能问题。如果开发人员使用这样的查询,然后不使用大量记录进行测试,他们可能会遇到困难。这更加令人惊讶,因为微软总是谈论大数据,但随后 LINQ 在实体框架核心提供了一个工具,只是在某些方面没有大数据能力。
这种 RAM 操作只能通过新的提供者架构在实体框架核心中进行,这使得提供者可以决定在 RAM 中执行某些操作。微软称之为客户评估。
软件开发人员可以通过关闭客户端评估来防止这些性能问题。这可以通过ConfigureWarnings()方法实现,它提供了开发人员在OnConfiguring()方法中获得的DbContextOptionsBuilder对象。以下配置导致每个客户端评估触发一个运行时错误,如图 8-2 所示。默认情况下,实体框架核心仅记录客户端评估(参见第十二章记录)。
图 8-2
Runtime error raised on a client evaluation if disabled
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer(_DbConnection);
builder.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
}
使用错误的命令顺序
然而,有时软件开发者自己在 RAM 中而不是在数据库管理系统中进行操作。在清单 8-12 中,ToList()被过早调用,并且查询包含类型List<Flight>而不是类型IQueryable<Flight>的对象。因此,基于出发地和目的地的过滤器和排序发生在 RAM 中,其中 LINQ 为对象。
Note
LINQ 在 RAM (LINQ 到对象)和 LINQ 在实体框架/实体框架核心中使用相同的查询语法。因此,你无法在一个程序代码行中看到,它是在 ram 中执行还是在数据库管理系统中执行。这总是取决于基本集合的数据类型(也就是说,LINQ 查询中的in之后是什么)。
public static void LINQ_CompositionWrongOrder()
{
CUI.MainHeadline(nameof(LINQ_Composition));
var departure = "";
var destination = "Rome";
// Create context instance
using (var ctx = new WWWingsContext())
{
// Define query (ToList() ist WRONG here!)
var query = (from x in ctx.FlightSet
where x.FreeSeats > 0
select x).ToList();
// Conditional addition of further conditions
if (!String.IsNullOrEmpty(departure)) query = query.Where(x => x.Departure == departure).ToList();
if (!String.IsNullOrEmpty(destination)) query = query.Where(x => x.Destination == destination).ToList();
// Sorting
var querySorted = from x in query
orderby x.Date, x.Departure
select x;
// The query shoud execute here, but it is already executed
List<Flight> flightSet = querySorted.ToList();
// Count loaded objects
long c = flightSet.Count;
Console.WriteLine("Number of loaded flights: " + c);
// Print result
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
}
} // End using-Block -> Dispose()
}
Listing 8-12ToList() Is Set Too Early and Causes the Following Queries to Execute in RAM
在 LINQ 使用自定义函数
Entity Framework Core 中新的提供者架构为在 LINQ 查询中整合您自己的函数提供了可能性。当然,这部分查询是在 RAM 中执行的。例如,清单 8-13 中的查询包含它自己的GetNumberofDaysUntil()方法。此外,在这种情况下,只对数据库中的FreeSeats列执行过滤器。
Note
从 C# 6.0 开始就存在的本地函数不能在 LINQ 命令中调用。
private static int GetNumberOfDaysUntil(DateTime t)
{
return (t - DateTime.Now).Days;
}
public static void LINQ_CustomFunction()
{
CUI.MainHeadline("Query with Custom Function - RAM :-(");
using (var ctx = new WWWingsContext())
{
var q4 = from f in ctx.FlightSet
where f.FreeSeats > 0 &&
GetNumberOfDaysUntil(f.Date) > 10
orderby f.FlightNo
select f;
List<Flight> l4 = q4.Take(10).ToList();
Console.WriteLine("Count: " + l4.Count);
foreach (var f in l4)
{
Console.WriteLine(f);
}
}
}
Listing 8-13Custom Functions in LINQ
解决分组问题
在 Entity Framework Core 1.x 和 2.0 中,没有将 LINQ 分组转换为 SQL,而是将记录分组到 RAM 中,这对于许多现实场景来说是绝对不可接受的。
实际上,需要一种在相应的数据库管理系统中执行分组的解决方案。不幸的是,使用 LINQ,这在实体框架核心 1.x 和 2.0 中不能实现。但是 SQL 的使用也带来了挑战,因为 Entity Framework Core 还不支持将 SQL 查询的结果映射到任何类型,而只支持映射到实体类。
Note
微软将在实体框架核心 2.1 版本中引入GroupBy翻译(见附录 C ),这将使这些变通办法变得过时。
映射到不存在的类型
清单 8-14 中使用FromSql()的代码不幸不是一个解决方案。它未能执行FromSql()并导致以下运行时错误:“无法为‘departure group’创建 DbSet,因为该类型未包含在上下文的模型中。”然而,错误消息提示了另一个有效的技巧(参见下面的“挑战:迁移”一节)。
public static void GroupBy_SQL_NonEntityType()
{
// Get the number of flights per departure
using (var ctx = new WWWingsContext())
{
// Map SQL to non-entity class
Console.WriteLine(ctx.Database.GetType().FullName);
ctx.Log();
var sql = "SELECT Departure, COUNT(FlightNo) AS FlightCount FROM Flight GROUP BY Departure";
// ERROR!!! Cannot create a DbSet for 'Group' because this type is not included in the model for the context."
var groupSet = ctx.Set<DepartureGroup>().FromSql(sql);
// Output
foreach (var g in groupSet)
{
Console.WriteLine(g.Departure + ": " + g.FlightCount);
}
}
}
Listing 8-14No Solution to the GroupBy Issue
为数据库视图结果创建实体类
因为在非实体类型上不能实现与FromSql()的映射,所以您必须为分组结果创建一个伪实体类,其名称和类型属性必须与分组结果的列相匹配。这个实体类还需要一个符合约定的主键(ID 为classnameID),或者必须使用[Key]或 Fluent API 的HasKey()来指定。见清单 8-15 。
namespace BO
{
public class DepartureGrouping
{
[Key] // must have a PK
public string Departure { get; set; }
public int FlightCount { get; set; }
}
...
}
Listing 8-15Entity Class with Two Properties for the Grouping Result
在上下文类中包含实体类
分组结果的伪实体类必须通过DbSet<T>作为实体类包含在上下文类中,如清单 8-16 所示。
public class WWWingsContext: DbContext
{
#region tables
public DbSet<Flight> FlightSet {get; set; }
public DbSet<Pilot> PilotSet {get; set; }
public DbSet<Passenger> PassengerSet {get; set; }
public DbSet<Airport> AirportSet {get; set; }
public DbSet<Booking> BookingSet {get; set; }
public DbSet<AircraftType> AircraftTypeSet {get; set; }
#endregion
#region grouping results (pseudo-entities)
public DbSet<DepartureGrouping> DepartureGroupingSet {get; set; } // for grouping
#endregion ...
}
Listing 8-16Including the Entity Class for the Database View in the Context Class
使用伪实体类
实体类DepartureGrouping现在可以用作FromSQL()中的返回类型,如清单 8-17 所示。
public static void GroupBy_SQL_Trick()
{
// Get the number of flights per departure
using (var ctx = new WWWingsContext())
{
Console.WriteLine(ctx.Database.GetType().FullName);
ctx.Log();
// Map SQL to entity class
var sql = "SELECT Departure, COUNT(FlightNo) AS FlightCount FROM Flight GROUP BY Departure";
var groupSet = ctx.Set<BO.DepartureGrouping>().FromSql(sql).Where(x=>x.FlightCount>5).OrderBy(x=>x.FlightCount);
// Output
foreach (var g in groupSet)
{
Console.WriteLine(g.Departure + ": " + g.FlightCount);
}
}
}
Listing 8-17Use of the Pseudo-Entity Class
图 8-3 显示了输出。
图 8-3
Output of Listing 8-17
挑战:迁移
如您所见,使用分组结果需要一些手工操作。不幸的是,除了打字工作之外,数据库模式迁移还有另一个挑战。
如果您在上下文中添加了数据库视图的伪实体类之后创建了一个模式迁移类,您将会注意到实体框架核心不期望地想要为数据库中的伪实体类创建一个表(参见清单 8-18 中的CreateTable())。这是正确的,因为我假设DepartureGrouping是一张桌子。然而,不希望为分组结果创建表格。
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace DA.Migrations
{
public partial class v3 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DepartureGrouping",
columns: table => new
{
Departure = table.Column<string>(nullable: false),
FlightCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DepartureGrouping", x => x.Departure);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DepartureGrouping");
}
}
}
Listing 8-18Entity Framework Core Creates a CreateTable() for the Pseudo-Entity in the Schema Migration (Not Desirable)
这种情况下有三种可能的解决方案:
- 您可以创建表,但不使用它。
- 您可以从迁移类中手动删除
Up()方法中的CreateTable()和Down()中对应的DropTable()。 - 您可以欺骗实体框架核心,使其在开发时而不是运行时创建迁移步骤时忽略实体类
DepartureStatistics。
清单 8-19 展示了如何实现这个技巧。作为创建或删除模式迁移的一部分,实体框架核心实例化上下文类,并调用OnModelCreating()。然而,在开发时,这不是通过应用的实际起始点发生的(然后应用将启动),而是通过在命令行工具ef.exe中托管带有上下文类的 DLL。因此,在OnModelCreating()中,您要检查当前进程的名称是否为ef。如果是这样,那么应用不在运行时,而您在开发环境中,想要用Ignore()忽略数据库视图。然而,在应用运行时,不会执行Ignore()方法,因此可以通过实体类使用数据库视图。
if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower() == "ef")
{
modelBuilder.Ignore<DepartureGrouping>();
}
Listing 8-19Entity Framework Core Should Ignore the Entity Class for the Database View at Development Time Only
Alternative Trick
如果进程名称的查询对您来说太不确定,因为 Microsoft 可以更改这个名称,您可以在上下文类中以静态属性的形式使用一个开关(例如,IsRuntime)。默认情况下,IsRuntime为 false,并忽略伪实体类。然而,在运行时,在上下文类第一次实例化之前,IsRuntime被设置为 true。
使用数据库视图分组
在 Entity Framework Core 1.x 和 2.0 中,通过使用数据库视图,解决GroupBy问题的方式略有不同。这里您定义了一个数据库视图,它进行分组并返回分组结果。
然而,因为这些版本的实体框架核心也不支持数据库视图,所以同样的技巧仍然适用于表示数据库视图结果的实体类。重要的是,您不能再选择在模式迁移中创建表,因为已经有一个同名的数据库视图,这会导致命名冲突。
您可以在第十八章(数据库视图的映射)中找到关于使用数据库视图的详细信息。
LINQ 语法概述
本节介绍了 LINQ 最重要的命令,并附有有意义的例子作为快速参考。所有查询都在万维网之翼版本 2 的对象模型上执行,如图 8-4 所示。对于这些类中的每一个,在数据库中都有一个同名的对应表。
图 8-4
Object model for the following LINQ examples
所有命令都基于实体框架核心上下文的先前实例化。
WWWingsContext ctx = new WWWingsContext();
除了 LINQ 命令之外,还显示了可选的 lambda 符号和生成的 SQL 命令。对于 LINQ 和 lambda 符号,产生的 SQL 命令总是相同的;因此,这里只重印一次。
Tip
有关基本 LINQ 命令的更详细示例集合,请参见 https://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b 。
简单的选择命令(所有记录)
实体框架核心支持ToArray()、ToList()、ToDictionary()和ToLookup()将查询转换成一组对象。
CUI.Headline("All records as Array<T>");
Flight[] flightSet0a = (from f in ctx.FlightSet select f).ToArray();
Flight[] flightSet0b = ctx.FlightSet.ToArray();
CUI.Headline("All records as List<T>");
List<Flight> flightSet1a = (from f in ctx.FlightSet select f).ToList();
List<Flight> flightSet1b = ctx.FlightSet.ToList();
CUI.Headline("All records as Dictionary<T, T>");
Dictionary<int, Flight> flightSet2a = (from f in ctx.FlightSet select f).ToDictionary(f=>f.FlightNo, f=>f);
Dictionary<int, Flight> flightSet2b = ctx.FlightSet.ToDictionary(f => f.FlightNo, f => f);
CUI.Headline("All records as ILookup<T, T>");
ILookup<int, Flight> flightSet2c = (from f in ctx.FlightSet select f).ToLookup(f => f.FlightNo, f => f);
ILookup<int, Flight> flightSet2d = ctx.FlightSet.ToLookup(f => f.FlightNo, f => f);
在所有八种情况下,以下 SQL 语句都被发送到 DBMS:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
条件(在哪里)
List<Flight> flightSet3a = (from f in ctx.FlightSet
where f.Departure == "Berlin" &&
(f.Destination.StartsWith("Rome") || f.Destination.Contains("Paris"))
&& f.FreeSeats > 0
select f)
.ToList();
List<Flight> flightSet3b = ctx.FlightSet.Where(f => f.Departure == "Berlin" &&
(f.Destination.StartsWith("Rome") || f.Destination.Contains("Paris"))
&& f.FreeSeats > 0)
.ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE (([f].[Departure] = N'Berlin') AND (([f].[Destination] LIKE N'Rome' + N'%' AND (LEFT([f].[Destination], LEN(N'Rome')) = N'Rome')) OR (CHARINDEX(N'Paris', [f].[Destination]) > 0))) AND ([f].[FreeSeats] > 0)
包含(英寸)
ist<string> Orte = new List<string>() { "Berlin", "Hamburg", "Köln", "Berlin" };
List<Flight> flightSet4a = (from f in ctx.FlightSet
where Orte.Contains(f.Departure)
select f)
.ToList();
List<Flight> flightSet4b = ctx.FlightSet.Where(f => Orte.Contains(f.Departure)).ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE [f].[Departure] IN (N'Berlin', N'Hamburg', N'Köln', N'Berlin')
排序(排序依据)
CUI.Headline("Sorting");
List<Flight> flightSet5a = (from f in ctx.FlightSet
where f.Departure == "Berlin"
orderby f.Date, f.Destination, f.FreeSeats descending
select f).ToList();
List<Flight> flightSet5b = ctx.FlightSet.Where(f => f.Departure == "Berlin")
.OrderBy(f => f.Date)
.ThenBy(f => f.Destination)
.ThenByDescending(f => f.FreeSeats)
.ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE [f].[Departure] = N'Berlin'
ORDER BY [f].[FlightDate], [f].[Destination], [f].[FreeSeats] DESC
分页(Skip()和 Take())
CUI.Headline("Paging");
List<Flight> flightSet6a = (from f in ctx.FlightSet
where f.Departure == "Berlin"
orderby f.Date
select f).Skip(100).Take(10).ToList();
List<Flight> flightSet6b = ctx.FlightSet.Where(f => f.Departure == "Berlin")
.OrderBy(f => f.Date)
.Skip(100).Take(10).ToList();
实体框架知道正在使用的数据库引擎,并将尽可能以最有效的方式实现分页。对于较新的版本,支持行限制子句。对于较老的数据库,将使用更复杂的查询和带有rownumber()函数的通用表表达(CTE)风格的语法。这是由实体框架核心自动控制的。
exec sp_executesql N'SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE [f].[Departure] = N''Berlin''
ORDER BY [f].[FlightDate]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY',N'@__p_0 int,@__p_1 int',@__p_0=100,@__p_1=10
规划
List<Flight> flightSet7a = (from f in ctx.FlightSet
where f.Departure == "Berlin"
orderby f.Date
select new Flight()
{
FlightNo = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats,
Timestamp = f.Timestamp
}).ToList();
List<Flight> flightSet7b = ctx.FlightSet
.Where(f => f.Departure == "Berlin")
.OrderBy(f => f.Date)
.Select(f => new Flight()
{
FlightNo = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats,
Timestamp = f.Timestamp
}).ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[FlightDate] AS [Date], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[Timestamp]
FROM [Flight] AS [f]
WHERE [f].[Departure] = N'Berlin'
ORDER BY [Date]
聚合函数(Count()、Min()、Max()、Average()、Sum())
int agg1a = (from f in ctx.FlightSet select f).Count();
int? agg2a = (from f in ctx.FlightSet select f).Sum(f => f.FreeSeats);
int? agg3a = (from f in ctx.FlightSet select f).Min(f => f.FreeSeats);
int? agg4a = (from f in ctx.FlightSet select f).Max(f => f.FreeSeats);
double? agg5a = (from f in ctx.FlightSet select f).Average(f => f.FreeSeats);
int agg1b = ctx.FlightSet.Count();
int? agg2b = ctx.FlightSet.Sum(f => f.FreeSeats);
int? agg3b = ctx.FlightSet.Min(f => f.FreeSeats);
int? agg4b = ctx.FlightSet.Max(f => f.FreeSeats);
double? agg5b = ctx.FlightSet.Average(f => f.FreeSeats);
产生的 SQL 如下所示:
SELECT COUNT (*)
FROM [Flight] AS [f]
SELECT SUM([f].[FreeSeats])
FROM [Flight] AS [f]
SELECT MIN([f].[FreeSeats])
FROM [Flight] AS [f]
SELECT MAX([f].[FreeSeats])
FROM [Flight] AS [f]
SELECT AVG(CAST([f].[FreeSeats] AS float))
FROM [Flight] AS [f]
分组(GroupBy)
var group1a = (from f in ctx.FlightSet
group f by f.Departure into g
select new { City = g.Key, Count = g.Count(), Sum = g.Sum(f => f.FreeSeats), Avg = g.Average(f => f.FreeSeats) })
.ToList();
var group1b = ctx.FlightSet
.GroupBy(f => f.Departure)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
Sum = g.Sum(f => f.FreeSeats),
Avg = g.Average(f => f.FreeSeats)
}).ToList();
Note
LINQ 分组仍然在实体框架核心的 2.0 版本中的 RAM 中运行。在即将到来的 2.1 版本中(见附录 C ),这些应该被正确地翻译成 SQL。因此,在 SQL 1.0 到 2.0 版本中,分组应该直接用公式表示(参见第十五章)。
数据库管理系统当前接收到以下命令:
SELECT [f0].[FlightNo], [f0].[AircraftTypeID], [f0].[AirlineCode], [f0].[CopilotId], [f0].[FlightDate], [f0].[Departure], [f0].[Destination], [f0].[FreeSeats], [f0].[LastChange], [f0].[Memo], [f0].[NonSmokingFlight], [f0].[PilotId], [f0].[Price], [f0].[Seats], [f0].[Strikebound], [f0].[Timestamp], [f0].[Utilization]
FROM [Flight] AS [f0]
ORDER BY [f0].[Departure]
单个对象(SingleOrDefault()、FirstOrDefault())
Flight flight1a = (from f in ctx.FlightSet select f).SingleOrDefault(f => f.FlightNo == 101);
Flight flight1b = ctx.FlightSet.SingleOrDefault(f => f.FlightNo == 101);
两种情况下产生的 SQL 如下:
SELECT TOP(2) [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE [f].[FlightNo] = 101
Flight flight2a = (from f in ctx.FlightSet
where f.FreeSeats > 0
orderby f.Date
select f).FirstOrDefault();
Flight flight2b = ctx.FlightSet
.Where(f => f.FreeSeats > 0)
.OrderBy(f => f.Date)
.FirstOrDefault();
两种情况下产生的 SQL 如下:
SELECT TOP(1) [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization]
FROM [Flight] AS [f]
WHERE [f].[FreeSeats] > 0
ORDER BY [f].[FlightDate]
相关对象(Include())
List<Flight> flightDetailsSet1a = (from f in ctx.FlightSet
.Include(f => f.Pilot)
.Include(f => f.BookingSet).ThenInclude(b => b.Passenger)
where f.Departure == "Berlin"
orderby f.Date
select f)
.ToList();
List<Flight> flightDetailsSet1b = ctx.FlightSet
.Include(f => f.Pilot)
.Include(f => f.BookingSet).ThenInclude(b => b.Passenger)
.Where(f => f.Departure == "Berlin")
.OrderBy(f => f.Date)
.ToList();
Note
实体框架核心直接依次执行两条 SQL 语句,以避免连接。
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization], [f.Pilot].[PersonID], [f.Pilot].[Birthday], [f.Pilot].[DetailID], [f.Pilot].[Discriminator], [f.Pilot].[EMail], [f.Pilot].[GivenName], [f.Pilot].[PassportNumber], [f.Pilot].[Salary], [f.Pilot].[SupervisorPersonID], [f.Pilot].[Surname], [f.Pilot].[FlightHours], [f.Pilot].[FlightSchool], [f.Pilot].[LicenseDate], [f.Pilot].[PilotLicenseType]
FROM [Flight] AS [f]
INNER JOIN [Employee] AS [f.Pilot] ON [f].[PilotId] = [f.Pilot].[PersonID]
WHERE ([f.Pilot].[Discriminator] = N'Pilot') AND ([f].[Departure] = N'Berlin')
ORDER BY [f].[FlightDate], [f].[FlightNo]
SELECT [f.BookingSet].[FlightNo], [f.BookingSet].[PassengerID], [b.Passenger].[PersonID], [b.Passenger].[Birthday], [b.Passenger].[CustomerSince], [b.Passenger].[DetailID], [b.Passenger].[EMail], [b.Passenger].[GivenName], [b.Passenger].[Status], [b.Passenger].[Surname]
FROM [Booking] AS [f.BookingSet]
INNER JOIN [Passenger] AS [b.Passenger] ON [f.BookingSet].[PassengerID] = [b.Passenger].[PersonID]
INNER JOIN (
SELECT DISTINCT [f0].[FlightNo], [f0].[FlightDate]
FROM [Flight] AS [f0]
INNER JOIN [Employee] AS [f.Pilot0] ON [f0].[PilotId] = [f.Pilot0].[PersonID]
WHERE ([f.Pilot0].[Discriminator] = N'Pilot') AND ([f0].[Departure] = N'Berlin')
) AS [t] ON [f.BookingSet].[FlightNo] = [t].[FlightNo]
ORDER BY [t].[FlightDate], [t].[FlightNo]
内部连接
如果存在导航关系,则不需要显式连接操作(参见“相关对象(Include())”)。在以下示例中,为了构建一个没有导航关系的案例,将搜索与飞行员具有相同 ID 的所有航班:
var flightDetailsSet2a = (from f in ctx.FlightSet
join p in ctx.PilotSet
on f.FlightNo equals p.PersonID
select new { Nr = f.FlightNo, flight = f, Pilot = p })
.ToList();
var flightDetailsSet2b = ctx.FlightSet
.Join(ctx.PilotSet, f => f.FlightNo, p => p.PersonID,
(f, p) => new { Nr = f.FlightNo, flight = f, Pilot = p })
.ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo] AS [Nr], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization], [p].[PersonID], [p].[Birthday], [p].[DetailID], [p].[Discriminator], [p].[EMail], [p].[GivenName], [p].[PassportNumber], [p].[Salary], [p].[SupervisorPersonID], [p].[Surname], [p].[FlightHours], [p].[FlightSchool], [p].[LicenseDate], [p].[PilotLicenseType]
FROM [Flight] AS [f]
INNER JOIN [Employee] AS [p] ON [f].[FlightNo] = [p].[PersonID]
WHERE [p].[Discriminator] = N'Pilot'
交叉连接(笛卡尔乘积)
var flightDetailsSet3a = (from f in ctx.FlightSet
from b in ctx.BookingSet
from p in ctx.PassengerSet
where f.FlightNo == b.FlightNo && b.PassengerID == p.PersonID && f.Departure == "Rome"
select new { flight = f, passengers = p })
.ToList();
var flightDetailsSet3b = ctx.FlightSet
.SelectMany(f => ctx.BookingSet, (f, b) => new { f = f, b = b})
.SelectMany(z => ctx.PassengerSet, (x, p) => new {x = x, p = p})
.Where(y => ((y.x.f.FlightNo == y.x.b.FlightNo) &&
(y.x.b.PassengerID == y.p.PersonID)) && y.x.f.Departure == "Rome")
.Select(z => new {flight = z.x.f, passengers = z.p } )
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[AircraftTypeID], [f].[AirlineCode], [f].[CopilotId], [f].[FlightDate], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[LastChange], [f].[Memo], [f].[NonSmokingFlight], [f].[PilotId], [f].[Price], [f].[Seats], [f].[Strikebound], [f].[Timestamp], [f].[Utilization], [p].[PersonID], [p].[Birthday], [p].[CustomerSince], [p].[DetailID], [p].[EMail], [p].[GivenName], [p].[Status], [p].[Surname]
FROM [Flight] AS [f]
CROSS JOIN [Booking] AS [b]
CROSS JOIN [Passenger] AS [p]
WHERE (([f].[FlightNo] = [b].[FlightNo]) AND ([b].[PassengerID] = [p].[PersonID])) AND ([f].[Departure] = N'Rome')
加入一个团体
var flightDetailsSet4a = (from b in ctx.BookingSet
join f in ctx.FlightSet on b.FlightNo equals f.FlightNo
join p in ctx.PassengerSet on b.PassengerID equals p.PersonID
where f.Departure == "Berlin"
group b by b.Flight into g
select new { flight = g.Key, passengers = g.Select(x => x.Passenger) })
.ToList();
var flightDetailsSet4b = ctx.BookingSet
.Join(ctx.FlightSet, b => b.FlightNo, f => f.FlightNo, (b, f) => new { b = b, f = f })
.Join(ctx.PassengerSet, x => x.b.PassengerID, p => p.PersonID, (x, p) => new { x = x, p = p })
.Where(z => (z.x.f.Departure == "Berlin"))
.GroupBy(y => y.x.b.Flight, y => y.x.b)
.Select(g => new { flight = g.Key, passengers = g.Select(x => x.Passenger) })
.ToList();
两种情况下产生的 SQL 如下:
SELECT [b0].[FlightNo], [b0].[PassengerID], [b.Flight0].[FlightNo], [b.Flight0].[AircraftTypeID], [b.Flight0].[AirlineCode], [b.Flight0].[CopilotId], [b.Flight0].[FlightDate], [b.Flight0].[Departure], [b.Flight0].[Destination], [b.Flight0].[FreeSeats], [b.Flight0].[LastChange], [b.Flight0].[Memo], [b.Flight0].[NonSmokingFlight], [b.Flight0].[PilotId], [b.Flight0].[Price], [b.Flight0].[Seats], [b.Flight0].[Strikebound], [b.Flight0].[Timestamp], [b.Flight0].[Utilization], [f0].[FlightNo], [f0].[AircraftTypeID], [f0].[AirlineCode], [f0].[CopilotId], [f0].[FlightDate], [f0].[Departure], [f0].[Destination], [f0].[FreeSeats], [f0].[LastChange], [f0].[Memo], [f0].[NonSmokingFlight], [f0].[PilotId], [f0].[Price], [f0].[Seats], [f0].[Strikebound], [f0].[Timestamp], [f0].[Utilization], [p0].[PersonID], [p0].[Birthday], [p0].[CustomerSince], [p0].[DetailID], [p0].[EMail], [p0].[GivenName], [p0].[Status], [p0].[Surname]
FROM [Booking] AS [b0]
INNER JOIN [Flight] AS [b.Flight0] ON [b0].[FlightNo] = [b.Flight0].[FlightNo]
INNER JOIN [Flight] AS [f0] ON [b0].[FlightNo] = [f0].[FlightNo]
INNER JOIN [Passenger] AS [p0] ON [b0].[PassengerID] = [p0].[PersonID]
WHERE [f0].[Departure] = N'Berlin'
ORDER BY [b.Flight0].[FlightNo]
子查询(子选择)
Note
实体框架和实体框架核心子查询都是针对主数据库管理系统查询的每个结果数据记录单独发送的。这可能会导致严重的性能问题!
List<Flight> flightDetailsSet5a = (from f in ctx.FlightSet
where f.FlightNo == 101
select new Flight()
{
FlightNo = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats,
Timestamp = f.Timestamp,
Pilot = (from p in ctx.PilotSet where
p.PersonID == f.PilotId select p)
.FirstOrDefault(),
Copilot = (from p in ctx.PilotSet where
p.PersonID == f.CopilotId select p)
.FirstOrDefault(),
}).ToList();
List<Flight> flightDetailsSet5b = ctx.FlightSet.Where(f => f.FlightNo == 101)
.Select(f =>new Flight()
{
FlightNo = f.FlightNo,
Date = f.Date,
Departure = f.Departure,
Destination = f.Destination,
FreeSeats = f.FreeSeats,
Timestamp = f.Timestamp,
Pilot = ctx.PilotSet
.Where(p => (p.PersonID == f.PilotId))
.FirstOrDefault(),
Copilot = ctx.PilotSet
.Where(p => (p.PersonID) == f.CopilotId)
.FirstOrDefault()
}
).ToList();
两种情况下产生的 SQL 如下:
SELECT [f].[FlightNo], [f].[FlightDate] AS [Date], [f].[Departure], [f].[Destination], [f].[FreeSeats], [f].[Timestamp], [f].[PilotId], [f].[CopilotId]
FROM [Flight] AS [f]
WHERE [f].[FlightNo] = 101
exec sp_executesql N'SELECT TOP(1) [p].[PersonID], [p].[Birthday], [p].[DetailID], [p].[Discriminator], [p].[EMail], [p].[GivenName], [p].[PassportNumber], [p].[Salary], [p].[SupervisorPersonID], [p].[Surname], [p].[FlightHours], [p].[FlightSchool], [p].[LicenseDate], [p].[PilotLicenseType]
FROM [Employee] AS [p]
WHERE ([p].[Discriminator] = N''Pilot'') AND ([p].[PersonID] = @_outer_PilotId)',N'@_outer_PilotId int',@_outer_PilotId=23
exec sp_executesql N'SELECT TOP(1) [p0].[PersonID], [p0].[Birthday], [p0].[DetailID], [p0].[Discriminator], [p0].[EMail], [p0].[GivenName], [p0].[PassportNumber], [p0].[Salary], [p0].[SupervisorPersonID], [p0].[Surname], [p0].[FlightHours], [p0].[FlightSchool], [p0].[LicenseDate], [p0].[PilotLicenseType]
FROM [Employee] AS [p0]
WHERE ([p0].[Discriminator] = N''Pilot'') AND ([p0].[PersonID] = @_outer_CopilotId)',N'@_outer_CopilotId int',@_outer_CopilotId=3
九、对象关系和加载策略
对象模型描述了不同类的实例之间的关系(例如,Flight和Pilot之间的关系)或者同一类的其他实例之间的关系(例如,参见Employees类中的Supervisor属性)。何时以及如何加载关系对象的问题不仅对软件开发人员至关重要,而且对应用的性能也至关重要。
装载策略概述
经典实体框架支持四种连接对象加载策略:自动延迟加载、显式加载、急切加载和预加载与关系修复(见图 9-1 )。在 Entity Framework Core 1.0 中,只有急切的加载和预加载。实体框架核心 1.1 引入了显式加载。实体框架 Core 2.0 中还不存在惰性加载,但在 2.1 版本中将会引入(见附录 C )。
图 9-1
Loading strategies in the Entity Framework 1.0 to 6.x. Entity Framework Core currently supports only three strategies; lazy loading is missing.
查看默认行为
默认情况下,实体框架核心将自己限制为在查询中加载实际请求的对象,而不会自动加载链接的对象。以下 LINQ 查询仅加载飞行对象。与航班相关联的Pilot、Booking、Airline和AircraftType类型的对象不会自动加载。
List<Flight> list = (from x in ctx.FlightSet
where x.Departure == "Berlin" &&
x.FreeSeats > 0
orderby x.Date, x.Departure
select x).ToList();
对于默认设置来说,加载链接记录(称为快速加载)并不是一个好主意,因为在这种情况下,会加载以后不需要的数据。此外,关联的记录有关系;例如,预订与乘客相关。乘客也可以预订其他航班。如果您递归地加载所有这些相关的记录,那么在图 9-2 中的对象模型的例子中,您几乎肯定会将几乎所有的记录加载到 RAM 中,因为许多乘客通过共享航班与其他乘客相连。所以急切加载并不是一个好的默认设置。
图 9-2
Object model for the management of flights and related objects
即使你使用Find()加载单个对象,也不会加载链接的记录(见清单 9-1 )。图 9-3 显示输出。
图 9-3
Output of Listing 9-1
public static void Demo_LazyLoading()
{
CUI.MainHeadline(nameof(Demo_LazyLoading));
using (var ctx = new WWWingsContext())
{
// Load only the flight
var f = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
if (f.Pilot != null) Console.WriteLine($"Pilot: {f.Pilot.Surname} has {f.Pilot.FlightAsPilotSet.Count} flights as pilot!");
else Console.WriteLine("No pilot assigned!");
if (f.Copilot != null) Console.WriteLine($"Copilot: {f.Copilot.Surname} has {f.Copilot.FlightAsCopilotSet.Count} flights as copilot!");
else Console.WriteLine("No copilot assigned!");
if (f.BookingSet is null) CUI.PrintError("No bookings :-(");
else
{
Console.WriteLine("Number of passengers on this flight: " + f.BookingSet.Count);
Console.WriteLine("Passengers on this flight:");
foreach (var b in f.BookingSet)
{
Console.WriteLine("- Passenger #{0}: {1} {2}", b.Passenger.PersonID, b.Passenger.GivenName, b.Passenger.Surname);
}
}
}
}
Listing 9-1Unsuccessful Attempt to Access Connected Objects in Entity Framework Core
还没有偷懒加载
前面的示例在第一步中只加载经典实体框架中明确请求的航班,但是在接下来的程序代码行中,飞行员和副驾驶信息(以及他们的其他航班)以及带有乘客数据的预订将通过延迟加载加载。实体框架会一个接一个地向数据库发送大量的SELECT命令。使用多少命令取决于该航班的乘客数量。
然而,在实体框架核心的情况下,列表 9-1 不加载飞行员、副驾驶或乘客。微软还没有实现实体框架核心的延迟加载。
Preview
实体框架核心版本 2.1 计划提供对延迟加载的基本支持;参见附录 C 。
延迟加载涉及一个特殊的实现挑战,因为 OR 映射器必须捕获对任何对象引用的任何访问,以便根据需要重新加载连接的对象。这种拦截是通过对单个引用和类集合使用特定的类来完成的。
在经典的实体框架中,您可以通过使用某些支持延迟加载的类和大多数不可见的运行时代理对象来实现延迟加载。两者都将在实体框架核心 2.1 中提供。
当您不需要在 Entity Framework Core 中预加载链接的记录时,使用惰性加载会非常好。一个典型的例子是屏幕上的主从视图。如果有许多主记录,预先为每个主记录加载明细记录会浪费时间。相反,您将请求用户刚刚单击的主记录的详细记录。在经典的实体框架核心中,当点击主数据记录时,您可以通过延迟加载实现主从显示,而无需更多的程序代码。在 Entity Framework Core 1.x 和 2.0 中,不幸的是,您必须拦截点击并显式加载详细数据记录。
显式加载
在 2016 年发布的实体框架核心 1.1 版中,微软对显式重载功能进行了改造。您使用方法Reference()(针对单个对象)、Collection()(针对集合),然后使用Load()来指定相关对象应该被加载。
然而,这些方法在实体对象本身上是不可用的,而是类EntityEntry<T>的一部分,它是由类DbContext中的方法Entry()获得的(参见清单 9-2 )。使用IsLoaded()可以检查对象是否已经加载。IsLoaded()即使数据库中没有匹配的对象,也返回 true。因此,它不指示导航关系是否有计数器对象;它指示在当前上下文实例中是否曾经为该实例加载过合适的对象。因此,如果在清单 9-2 中,101 航班已经有一个指定的飞行员(默克尔夫人)但没有副驾驶,这将导致图 9-4 中的输出。
Important
理解Load()的每一次执行都将导致向数据库管理系统显式提交一个 SQL 命令是很重要的。
图 9-4
Output of Listing 9-2
public static void Demo_ExplizitLoading_v11()
{
CUI.MainHeadline(nameof(Demo_ExplizitLoading_v11));
using (var ctx = new WWWingsContext())
{
// Load only the flight
var f = ctx.FlightSet
.SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
// Now load the pilot and copilot
if (!ctx.Entry(f).Reference(x => x.Pilot).IsLoaded)
ctx.Entry(f).Reference(x => x.Pilot).Load();
if (!ctx.Entry(f).Reference(x => x.Copilot).IsLoaded)
ctx.Entry(f).Reference(x => x.Copilot).Load();
// Check if loaded
if (ctx.Entry(f).Reference(x => x.Pilot).IsLoaded) Console.WriteLine("Pilot is loaded!");
if (ctx.Entry(f).Reference(x => x.Copilot).IsLoaded) Console.WriteLine("Copilot is loaded!");
if (f.Pilot != null) Console.WriteLine($"Pilot: {f.Pilot.Surname} has {f.Pilot.FlightAsPilotSet.Count} flights as pilot!");
else Console.WriteLine("No pilot assigned!");
if (f.Copilot != null) Console.WriteLine($"Copilot: {f.Copilot.Surname} has {f.Copilot.FlightAsCopilotSet.Count} flights as copilot!");
else Console.WriteLine("No copilot assigned!");
// No download the booking list
if (!ctx.Entry(f).Collection(x => x.BookingSet).IsLoaded)
ctx.Entry(f).Collection(x => x.BookingSet).Load();
Console.WriteLine("Number of passengers on this flight: " + f.BookingSet.Count);
Console.WriteLine("Passengers on this flight:");
foreach (var b in f.BookingSet)
{
// Now load the passenger object for this booking
if (!ctx.Entry(b).Reference(x => x.Passenger).IsLoaded)
ctx.Entry(b).Reference(x => x.Passenger).Load();
Console.WriteLine("- Passenger #{0}: {1} {2}", b.Passenger.PersonID, b.Passenger.GivenName, b.Passenger.Surname);
}
}
}
Listing 9-2With Explicit Reloading, Entity Framework Core Sends a Lot of Individual SQL Commands to the Database
急切装载
和经典的实体框架一样,实体框架核心支持急切加载。但是,语法有了一点变化。
在经典的实体框架 1.0 和 4.0 版本中(从来没有 2.0 和 3.0 版本),你可以用Include()指定一个只有导航属性名称的字符串;编译器没有检查该字符串。从第三个版本开始(版本号 4.1),可以为导航属性指定健壮的 lambda 表达式,而不是字符串。对于多级加载路径,您必须嵌套 lambda 表达式并使用Select()方法。
在 Entity Framework Core 中,仍然有字符串和 lambda 表达式,但是 lambda 表达式的语法略有修改。新的扩展方法ThenInclude()可以用于嵌套关系,而不是使用Select(),就像ThenOrderBy()用于跨多列排序一样。清单 9-3 显示了带有以下链接数据的航班的紧急加载:
- 每个预订的预订和乘客信息:
Include(b => b.Bookings).ThenInclude (p => p.Passenger) - 飞行员和作为飞行员的其他航班:
Include(b => b.Pilot).ThenInclude (p => p.FlightAsPilotSet) - 副驾驶和作为副驾驶的其他航班:
Include (b => b.Co-Pilot).ThenInclude (p => p.FlightAsCopilotSet)
public static void Demo_EagerLoading()
{
CUI.MainHeadline(nameof(Demo_EagerLoading));
using (var ctx = new WWWingsContext())
{
var flightNo = 101;
// Load the flight and some connected objects via Eager Loading
var f = ctx.FlightSet
.Include(b => b.BookingSet).ThenInclude(p => p.Passenger)
.Include(b => b.Pilot).ThenInclude(p => p.FlightAsPilotSet)
.Include(b => b.Copilot).ThenInclude(p => p.FlightAsCopilotSet)
.SingleOrDefault(x => x.FlightNo == flightNo);
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
if (f.Pilot != null) Console.WriteLine($"Pilot: {f.Pilot.Surname} has {f.Pilot.FlightAsPilotSet.Count} flights as a pilot!");
else Console.WriteLine("No pilot assigned!");
if (f.Copilot != null) Console.WriteLine($"Copilot: {f.Copilot.Surname} has {f.Copilot.FlightAsCopilotSet.Count} flights as a Copilot!");
else Console.WriteLine("No Copilot assigned!");
Console.WriteLine("Number of passengers on this flight: " + f.BookingSet.Count);
Console.WriteLine("Passengers on this flight:");
foreach (var b in f.BookingSet)
{
Console.WriteLine("- Passenger #{0}: {1} {2}", b.Passenger.PersonID, b.Passenger.GivenName, b.Passenger.Surname);
}
}
}
Listing 9-3With Eager Loading You Can Use the Connected Objects in Entity Framework Core
图 9-5 显示了输出。Pilot和Copilot信息以及已预订的乘客名单均可用。
Attention
只有当类具有适当的属性或字段时,编译器才会用Include()和ThenInclude()进行检查。它不检查这是否也是另一个实体类的导航属性。如果它不是导航属性,则直到运行时才会出现以下错误:“属性 xy 不是实体类型‘ab’的导航属性。“Include(string)”方法只能与“.”一起使用导航属性名称的分隔列表。"
图 9-5
Output of Listing 9-3
然而,与传统的实体框架还有一个重要的区别。虽然 Entity Framework 1.0 到 6.x 版本只向数据库管理系统发送了一个大型的SELECT命令,但 Entity Framework 核心决定将查询分成四个步骤(参见图 9-6 ),如下所示:
- 首先,用 employee 表中的 join 装载航班,该表还包含飞行员信息(通过层次映射的表)。
- 第二步,实体框架核心加载副驾驶的其他航班。
- 第三步,实体框架核心加载飞行员的其他飞行。
- 在最后一步中,实体框架核心加载乘客详细信息。
图 9-6
SQL Server Profiler shows the four SQL commands that the eager loading example triggers in Entity Framework Core
这种策略比执行一个大的SELECT命令更快,该命令返回一大组重复记录的结果,然后 OR mapper 必须反汇编并清理重复的记录。将SELECT命令从实体框架核心中分离出来的策略也可能会更慢,因为数据库管理系统的每次往返都需要时间。在经典的实体框架中,您可以选择将一条急切的加载指令切割成多大的长度,以及将它加载到哪里。在实体框架核心中,您失去了对数据库管理系统往返次数的控制。
关系修复
关系修正是实体框架核心的一种机制,在经典的实体框架中已经存在。关系修正在 RAM 中的两个对象之间执行以下操作:
- 情况 1:当数据库中通过外键相关的两个对象被独立加载时,实体框架核心通过它们定义的导航属性建立两个对象之间的关系。
- 情况 2:当一个对象在 RAM 中被创建或者被修改为通过外键与 RAM 中的另一个对象相关时,实体框架核心通过它们定义的导航属性建立两者之间的关系。
- 情况 3a:当 RAM 中的一个对象通过导航连接到 RAM 中的另一个对象,并且与另一个方向的导航也存在双向关系时,实体框架核心也更新另一个导航属性。
- 情况 3b:当 RAM 中的一个对象使用外键属性连接到 RAM 中的另一个对象时,实体框架核心更新这两个对象上的其他导航属性。
Note
在情况 3a 和 3b 中,直到调用了ctx.ChangeTracker.DetectChanges()才会执行关系修正。与经典的实体框架不同,实体框架核心不再自动调用几乎所有 API 函数上的DetectChanges()调用,这是一个性能问题。实体框架核心只在ctx.SaveChanges()、ctx.Entry()、ctx.Entries()上运行DetectChanges()以及方法DbSet<T>().Add()。
案例 1 的示例
在清单 9-4 中,一个航班首先被加载到flight变量中。然后对于飞行,打印出Pilot对象的PilotId值。然而,Pilot对象此时还不可用,因为它没有与 flight 一起加载,并且实体框架核心目前不支持延迟加载。
然后使用 ID 将Flight对象的Pilot对象单独加载到变量pilot中。Pilot对象和Flight对象现在通常会彼此分离。然而,flight.Pilot的输出显示实体框架核心已经通过关系修复建立了关系。同样,后向关系也被记录了下来;pilot.FlightAsPilotSet显示之前加载的航班。
Note
该飞行员的进一步飞行可能包括在数据库中,但没有出现在这里,因为它们没有被加载。
public static void RelationshipFixUp_Case1()
{
CUI.MainHeadline(nameof(RelationshipFixUp_Case1));
using (var ctx = new WWWingsContext())
{
int flightNr = 101;
// 1\. Just load the flight
var flight = ctx.FlightSet.Find(flightNr);
// 2\. Output of the pilot of the Flight
Console.WriteLine(flight.PilotId + ": " + (flight.Pilot != null ? flight.Pilot.ToString() : "Pilot not loaded!"));
// 3\. Load the pilot separately
var pilot = ctx.PilotSet.Find(flight.PilotId);
// 4\. Output of the Pilot of the Flight: Pilot now available
Console.WriteLine(flight.PilotId + ": " + (flight.Pilot != null ? flight.Pilot.ToString() : "Pilot not loaded!"));
// 5\. Output the list of flights of this pilot
foreach (var f in pilot.FlightAsPilotSet)
{
Console.WriteLine(f);
}
}
}
Listing 9-4Relationship Fixup in Case 1
与经典实体框架一样,如果导航属性为空,实体框架核心还会将集合类型的实例分配给导航属性,作为关系修正的一部分。
无论您在声明导航属性时使用接口还是类作为类型,这种自动化都会存在。如果使用集合类,实体框架核心实例化集合类。在用接口类型声明导航属性的情况下,实体框架核心选择适当的集合类。使用ICollection<T>,选择类别HashSet<T>。用IList<T>选择List<T>类。
情况 2 的示例
在案例 2 的清单 9-5 中,加载了一个引导。起初,RAM 中没有来自飞行员的航班。然后创建一个新的航班,这个航班被分配加载的Pilot的PilotID。当调用ctx.FlightSet.Add()时,实体框架核心执行关系修正,从而填充Pilot对象的名为FlightAsPilotSet的导航属性和名为flying.Pilot的导航属性。
public static void RelationshipFixUp_Case2()
{
CUI.MainHeadline(nameof(RelationshipFixUp_Case2));
void PrintPilot(Pilot pilot)
{
CUI.PrintSuccess(pilot.ToString());
if (pilot.FlightAsPilotSet != null)
{
Console.WriteLine("Flights of this pilot:");
foreach (var f in pilot.FlightAsPilotSet)
{
Console.WriteLine(f);
}
}
else
{
CUI.PrintWarning("No flights!");
}
}
using (var ctx = new WWWingsContext())
{
// Load a Pilot
var pilot = ctx.PilotSet.FirstOrDefault();
// Print pilot and his flights
PrintPilot(pilot);
// Create a new flight for this pilot
var flight = new Flight();
flight.Departure = "Berlin";
flight.Destination = "Berlin";
flight.Date = DateTime.Now.AddDays(10);
flight.FlightNo = ctx.FlightSet.Max(x => x.FlightNo) + 1;
flight.PilotId = pilot.PersonID;
ctx.FlightSet.Add(flight);
// Print pilot and his flights
PrintPilot(pilot);
// Print pilot of the new flight
Console.WriteLine(flight.Pilot);
}
}
Listing 9-5Relationship Fixup in Case 2
情况 3 的示例
表 9-1 可选地包括情况 3a 和 3b。在本例中,首先加载一个flight。然后通过PilotID(情况 3a)或导航属性(情况 3b)加载并分配任何飞行员到加载的航班。然而,实体框架核心不会在这里自动运行关系修复操作。在情况 3a 中,flight.Pilot和pilot.FlightsAsPilotSet为空。这只随着ctx.ChangeTracker.DetectChanges()的召唤而改变。3b 情况下,Flight.Pilot手动填充,调用ctx.ChangeTracker.DetectChanges()后Pilot.FlightAsPilotSet变为填充。清单 9-6 显示了关系修复。
表 9-1
Comparing the Behavior of Cases 3a and 3b
| | 案例 3a | 案例 3b | | :-- | :-- | :-- | | 分配 | `flight.Pilot = pilot` | `flight.PilotId = pilot.PersonID` | | `Flight.Pilot` | `filled` | `filled after DetectChanges()` | | `Flight.PilotID` | `filled` | `filled` | | `Pilot.FlightAsPilotSet` | `filled after DetectChanges()` | `filled after DetectChanges()` | public static void RelationshipFixUp_Case3()
{
CUI.MainHeadline(nameof(RelationshipFixUp_Case3));
// Inline helper for output (>= C# 7.0)
void PrintflightPilot(Flight flight, Pilot pilot)
{
CUI.PrintSuccess(flight);
Console.WriteLine(flight.PilotId + ": " + (flight.Pilot != null ? flight.Pilot.ToString() : "Pilot not loaded!"));
CUI.PrintSuccess(pilot.ToString());
if (pilot.FlightAsPilotSet != null)
{
Console.WriteLine("Flights of this pilot:");
foreach (var f in pilot.FlightAsPilotSet)
{
Console.WriteLine(f);
}
}
else
{
CUI.PrintWarning("No flights!");
}
}
using (var ctx = new WWWingsContext())
{
int flightNr = 101;
CUI.Headline("Load flight");
var flight = ctx.FlightSet.Find(flightNr);
Console.WriteLine(flight);
// Pilot of this flight
Console.WriteLine(flight.PilotId + ": " + (flight.Pilot != null ? flight.Pilot.ToString() : "Pilot not loaded!"));
CUI.Headline("Load pilot");
var pilot = ctx.PilotSet.FirstOrDefault();
Console.WriteLine(pilot);
CUI.Headline("Assign a new pilot");
flight.Pilot = pilot; // Case 3a
//flight.PilotId = pilot.PersonID; // Case 3b
// Determine which relationships exist
PrintflightPilot(flight, pilot);
// Here you have to trigger the Relationshop fixup yourself
CUI.Headline("DetectChanges...");
ctx.ChangeTracker.DetectChanges();
// Determine which relationships exist
PrintflightPilot(flight, pilot);
}
}
Listing 9-6Relationship Fixup in Case 3
预加载关系修正
像经典的实体框架一样,实体框架核心支持另一种加载策略:预加载和 RAM 中的关系修复操作。您显式地为连接的对象发出几个 LINQ 命令,OR mapper 将新添加的对象在它们与那些已经在 RAM 中的对象具体化之后放在一起。在以下语句之后,当访问flight.Pilot和flight.Copilot时,101 航班以及 101 航班的Pilot和Copilot对象可以在 RAM 中找到:
var Flight = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == 101);
ctx.PilotSet.Where(p => p.FlightAsPilotSet.Any(x => x.FlightNo == 101) || p.FlightAsCopilotSet.Any(x => x.FlightNo == 101)).ToList();
当加载两个导频时,实体框架核心识别出 RAM 中已经有一个Flight对象,需要这两个导频作为Pilot或Copilot。然后用 RAM 中的两个Pilot对象编译Flight对象(通过关系修复,如前所述)。
虽然 Flight 101 的Pilot和Copilot对象是在前两行中专门加载的,但是您也可以使用关系修复进行缓存优化。清单 9-7 显示所有飞行员和部分航班都是满载的。对于每个加载的飞行,Pilot和Copilot对象都是可用的。当然,和缓存一样,这里需要更多一点的 RAM,因为你还会加载从不需要的Pilot对象。此外,您必须意识到,您可能会遇到一个时间性问题,因为依赖数据与主数据处于同一级别。但这一直是缓存的行为方式。但是,您可以节省数据库管理系统的往返行程,并提高其速度。
清单 9-7 还显示了当加载两个飞行员的信息时,您可以通过使用导航属性和Any()方法来避免实体框架核心中的连接操作符。Any()检查是否至少有一条记录符合或不符合条件。在前一种情况下,Pilot对象被分配一次作为您正在寻找的Flight的Pilot或Copilot就足够了。在其他情况下,如果想要处理一组满足或不满足条件的记录,可以使用 LINQ All()方法。
Note
值得注意的是,先前两个导频的加载和下一个示例中所有导频的加载都没有将 LINQ 查询的结果赋给变量。事实上,这是不必要的,因为实体框架核心(像经典的实体框架一样)在它的一级缓存中包含了对所有曾经被加载到上下文类的特定实例中的对象的引用。因此,关系修正在没有变量存储的情况下也能工作。给一个变量(List<Pilot> allPilot = ctx.PilotSet.ToList())赋值当然是无害的,但是如果你需要一个程序流中所有飞行员的列表,这可能是有用的。还应该注意的是,关系修正并不在上下文的所有实例中起作用。为此目的所需的二级缓存在实体框架核心中尚不可用,但可作为附加组件使用(参见第十七章和第二十章)。
public static void Demo_PreLoadingPilotenCaching()
{
CUI.MainHeadline(nameof(Demo_PreLoadingPilotenCaching));
using (var ctx = new WWWingsContext())
{
// 1\. Load ALL pilots
ctx.PilotSet.ToList();
// 2\. Load only several flights. The Pilot and Copilot object will then be available for every flight!
var FlightNrListe = new List<int>() { 101, 117, 119, 118 };
foreach (var FlightNr in FlightNrListe)
{
var f = ctx.FlightSet
.SingleOrDefault(x => x.FlightNo == FlightNr);
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} free seats!");
if (f.Pilot != null) Console.WriteLine($"Pilot: {f.Pilot.Surname} has {f.Pilot.FlightAsPilotSet.Count} flights as pilot!");
else Console.WriteLine("No pilot assigned!");
if (f.Copilot != null)
Console.WriteLine($"Copilot: {f.Copilot.Surname} has {f.Copilot.FlightAsCopilotSet.Count} flights as copilot!");
else Console.WriteLine("No copilot assigned!");
}
}
}
Listing 9-7Caching of All Pilots
清单 9-8 显示了将所有飞行员和乘客数据加载到一次飞行中的原始任务的重新设计,这次是预加载和关系修正,而不是急切加载。在这里,航班、飞行员、飞行员的其他航班、预订和乘客被单独加载。因此,代码向数据库管理系统发送了五个SELECT命令(与解决方案在急切加载时发送的四个SELECT命令相反),但是避免了一些连接。图 9-7 显示了输出。
图 9-7
SQL Server Profiler shows the five SQL commands in Listing 9-8
/// <summary>
/// Provides Pilot, booking and passenger information via Preloading / RelationshipFixup
/// </ summary>
public static void Demo_PreLoading()
{
CUI.Headline ( "Demo_PreLoading");
using (var ctx = new WWWingsContext())
{
int Flight no = 101;
// 1\. Just load the Flight
var f = ctx.FlightSet
30.4 SingleOrDefault (x => x.FlightNo == FlightNo);
// 2\. Load both Pilots
ctx.PilotSet.Where (p => p.FlightAsPilotSet.Any (x => x.FlightNo == FlightNo) || p.FlightAsCopilotSet.Any (x => x.FlightNo == FlightNo)).ToList();
// 3\. Load other Pilots' Flights
ctx.FlightSet.Where (x => x.PilotId == f.PilotId || x.CopilotId == f.CopilotId).ToList();
// 4\. Loading bookings
ctx.BuchungSet.Where (x => x.FlightNo == FlightNo).ToList();
// 5\. Load passengers
ctx.PassengerSet.Where (p => p.BookingsAny (x => x.FlightNo == FlightNo)).ToList();
// not necessary: ctx.ChangeTracker.DetectChanges();
Console.WriteLine ($ "Flight No {f.FlightNo} from {f.Departure} to {f.Destination} has {f.FreeSeats} FreeSeats! ");
if (f.Pilot != null) Console.WriteLine ($ "Pilot: {f.Pilot.Name} has {f.Pilot.FlightAsPilotSet.Count} Flights as a Pilot! ");
else console.WriteLine ("No Pilot assigned!");
if (f.Copilot != null) Console.WriteLine ($ "Copilot: {f.Copilot.Name} has {f.Copilot.FlightAsCopilotSet.Count} Flights as copilot! ");
else console.WriteLine ("No Copilot assigned!");
Console.WriteLine ("Number of passengers on this Flight:" + f.BookingsCount);
Console.WriteLine ("Passengers on this Flight:");
foreach (var b in f.Bookings
{
Console.WriteLine ("- Passenger # {0}: {1} {2}", b.Passenger.PersonID, b.Passenger.First given name, b.Passenger.Nam
}
}
}
Listing 9-8Loading Flights, Pilots, Bookings, and Passengers in Separate LINQ Commands
如果下列一个或多个条件为真,则关系修复技巧具有正面效果:
- 主数据的结果集大,从属数据量小。
- 有几种不同的相关数据集可以预加载。
- 预加载的对象很少是可变(父)数据。
- 您在具有相同依赖数据的单个上下文实例中运行多个查询。
在速度比较中,即使在这里讨论的装载飞行员和乘客的情况下,它已经显示了预载的速度优势。图 9-8 所示的测量是为了避免 51 个周期的测量偏差,第一遍(实体框架核心上下文的冷启动,可能还有数据库)没有考虑在内。此外,所有屏幕版本都进行了扩展。
当然,您可以随意混合急切加载和预加载。然而,在实践中,你必须为每种情况找到最佳的比例。
图 9-8
Speed comparison of eager loading and preloading for 50 flight records with all the related pilots and passengers