实体框架核心现代数据访问教程-全-
实体框架核心现代数据访问教程(全)
一、实体框架核心简介
在本章中,您将了解实体框架核心,以及它如何成为的或映射器。NET(。NET 框架,。NET Core、Mono 和 Xamarin)。实体框架核心是 ADO.NET 实体框架的全新实现。
与...一起。NET 核心 1.0 版和 ASP.NET 核心 1.0 版,实体框架核心 1.0 版于 2016 年 6 月 27 日发布。2017 年 8 月 14 日发布 2.0 版本。2.1 版本正在开发中。
什么是对象关系映射器?
在数据库世界中,关系数据库很普遍。编程世界都是关于对象的。两个世界存在显著的语义和句法差异,称为阻抗不匹配; https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch
见。
将对象作为类的实例在内存中使用是面向对象编程(OOP)的核心。大多数应用还要求在对象中永久存储数据,尤其是在数据库中。基本上,有直接能够存储对象的面向对象数据库(OODBs ),但是 OODBs 到目前为止只有很小的分布。关系数据库更占优势,但是它们映射数据结构的方式不同于对象模型。
为了使面向对象系统中关系数据库的处理更加自然,软件行业多年来一直依赖于对象关系映射器。这些工具将面向对象世界中的概念,如类、属性或类之间的关系,转换成关系世界中相应的结构,如表、列和外键(见图 1-1 )。因此,开发人员可以留在面向对象的世界中,并指示 OR mapper 加载或存储关系数据库表中记录形式的某些对象。不太有趣的任务和容易出错的任务,比如手动创建INSERT
、UPDATE
和DELETE
语句,也由 OR mapper 处理,进一步减轻了开发人员的负担。
图 1-1
The OR mapper translates constructs of the OOP world to the relational world
对象模型和关系模型之间两个特别显著的区别是 N:M 关系和继承。虽然在对象模型中,可以通过一组相互的对象来映射对象之间的 N:M 关系,但是在关系数据库中需要一个中间表。关系数据库不支持继承。有不同的复制方式,但是在本书的后面你会学到更多。
或者地图绘制者。网络世界
当. NET 开发人员从带有DataReader
或DataSet
的数据库中读取数据时,开发人员此时并没有进行对象关系映射。虽然DataReader
和DataSet
是。NET 对象,它们只管理表结构。从对象模型的角度来看,DataReader
和DataSet
是无类型的、非特定的容器。只有当开发人员为存储在表中的结构定义了特定的类并将内容从DataSet
或DataReader
复制到这些特定的数据结构中时,开发人员才是在执行或映射。这种“手动”对象关系映射对于读访问(尤其是对于非常宽的表)来说是耗时、乏味且单调的编程工作。如果您想再次保存对象中的更改,这项工作将成为一项智力挑战,因为您必须能够识别哪些对象已经被更改。否则,您必须不断地重新保存所有数据,这在多用户环境中是荒谬的。
虽然 OR mappers 在 Java 世界中已经建立了很长时间,但是微软在很长一段时间内都未能将合适的产品推向市场。的第一个版本。NET 没有包含 OR 映射器,而是将自己局限于 XML 文档和关系模型之间的直接数据访问和映射。英寸 NET 3.5 中,有一个名为 LINQ 到 SQL 的 OR 映射器,但它仅限于 Microsoft SQL Server,并且有许多其他限制。
很多。NET 开发人员因此开始用辅助库和工具来简化这项工作。除了众所周知的或为。NET,您会发现许多内部解决方案正在构建中。
以下是第三方或地图 for.NET(其中一些开源):
- nhiberinated(无国籍人士)
- Telerik 数据访问(又名开放访问)
- 基因组
- LLBLGen Pro 公司
- 威尔逊
- 比音速稍慢的
- OBJ.NET
- DataObjects.NET
- 衣冠楚楚的
- 佩塔波
- 大量的
- xpo 快递
有了 LINQ 到 SQL、ADO.NET 实体框架和实体框架,微软自己现在有了三个 ORM 产品。该软件公司同时宣布,进一步的开发工作将集中在实体框架核心。
实体框架核心的版本历史
图 1-2 显示了实体框架核心的版本历史。
图 1-2
Entity Framework Core version history ( https://www.nuget.org/packages/Microsoft.EntityFrameworkCore
)
主要版本和次要版本(1.0、1.1 等)表示来自 Microsoft 的功能版本,修订版本(1.0.1、1.0.2 等)表示错误修复版本。这本书在讨论一个需要特定版本的函数时提到了最低版本。
Note
实体框架 Core 1.x 的实体框架核心工具于 2017 年 3 月 6 日发布,在实体框架 Core 1.1.1 和 Visual Studio 2017 的框架内。以前,这些工具只有“预览”版本。从 Entity Framework Core 2.0 开始,这些工具总是与新产品一起发布。
支持的操作系统
像核心产品家族中的其他产品一样,实体框架核心(以前的实体框架 7.0)是独立于平台的。已建立的对象关系映射器的核心版本不仅在“完整”上运行。NET 框架,但也在。NET Core 和 Mono,包括 Xamarin。这允许您在 Windows、Windows Phone/Mobile、Linux、macOS、iOS 和 Android 上使用实体框架核心。
支持。网络版本
实体框架核心 1.x 运行于。网芯 1.x,。NET Framework 4.5.1、Mono 4.6、Xamarin.iOS 10、Xamarin Android 7.0 或更高版本以及通用 Windows 平台(UWP)。
实体框架核心 2.0 是基于。NET Standard 2.0,因此需要下列之一。NET 实现(参见图 1-3 ):
图 1-3
Implementations of .NET Standard ( https://docs.microsoft.com/en-us/dotnet/standard/library
)
- 。NET Core 2.0(或更高版本)
- 。NET Framework 4.6.1(或更高版本)
- 单声道 5.4(或更高)
- Xamarin.iOS 10.14(或更高版本)
- Xamarin。Mac 3.8(或更高版本)
- Xamarin。Android 7.5(或更高版本)
- UWP 10.0.16299(或更高)
Note
微软认为这种限制是合理的。NET 标准在实体框架 Core 2.0 中的 https://github.com/aspnet/Announcements/issues/246
。除此之外,它可以显著减小 NuGet 包的大小。
支持的 Visual Studio 版本
要使用 Entity Framework Core 2.0/2.1,您需要 Visual Studio 2017 Update 3 或更高版本,即使您正在使用 classic 进行编程。NET Framework,因为只有此更新的 Visual Studio 才能识别。NET 标准 2.0 并理解这一点。NET Framework 4.6.1 和更高版本是。NET 标准 2.0。
支持的数据库
表 1-1 显示了实体框架核心支持的数据库管理系统,包括微软(SQL Server、SQL Compact 和 SQLite)和第三方供应商(PostgreSQL、DB2、Oracle、MySQL 等)的数据库管理系统。
在运行 Xamarin 或 Windows 10 UWP 应用的移动设备上,Entity Framework Core 1.x 只能处理本地数据库(SQLite)。随着……的引入。NET Standard 2.0,微软 SQL Server 客户端现在也可以在 Xamarin 和 Windows 10 UWP 上使用(从 2017 年秋季 Creators 更新开始)。
Entity Framework Core 1 . x/2 . x 版中尚未包含对 Redis、Azure 表存储等 NoSQL 数据库的计划支持,不过,GitHub 上有一个针对 MongoDB 的开源开发项目; https://github.com/crhairr/EntityFrameworkCore.MongoDb
见。
表 1-1
Available Database Drivers for Entity Framework Core
| 数据库ˌ资料库 | 公司/价格 | 统一资源定位器 | | :-- | :-- | :-- | | 搜寻配置不当的 | 微软/免费 | [`www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer`](http://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer) | | Microsoft SQL Server Compact 3.5 | 微软/免费 | [`www.nuget.org/packages/EntityFrameworkCore.SqlServerCompact35`](http://www.nuget.org/packages/EntityFrameworkCore.SqlServerCompact35) | | Microsoft SQL Server Compact 4.0 | 微软/免费 | [`www.nuget.org/packages/EntityFrameworkCore.SqlServerCompact40`](http://www.nuget.org/packages/EntityFrameworkCore.SqlServerCompact40) | | 数据库 | 微软/免费 | [`www.nuget.org/packages/Microsoft.EntityFrameworkCore.sqlite`](http://www.nuget.org/packages/Microsoft.EntityFrameworkCore.sqlite) | | 在记忆中 | 微软/免费 | [`www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory`](http://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory) | | 关系型数据库 | 甲骨文/免费 | [`www.nuget.org/packages/MySQL.Data.EntityFrameworkCore`](http://www.nuget.org/packages/MySQL.Data.EntityFrameworkCore) | | 一种数据库系统 | 开源团队 npgsql.org/free | [`www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL`](http://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL) | | DB2 | IBM/免费 | [`www.nuget.org/packages/EntityFramework.IBMDataServer`](http://www.nuget.org/packages/EntityFramework.IBMDataServer) | | MySQL、Oracle、PostgreSQL、SQLite、DB2、Salesforce、Dynamics CRM、SugarCRM、Zoho CRM、QuickBooks、FreshBooks、MailChimp、ExactTarget、Bigcommerce、Magento | Devart/$99 到$299 每种驱动程序类型 | [`www.devart.com/purchase.html#dotConnect`](http://www.devart.com/purchase.html#dotConnect) |Caution
由于提供程序接口中的重大更改,Entity Framework Core 1.x 提供程序与 Entity Framework Core 2.0 不兼容。所以,2.0 版需要新的提供者!
实体框架核心的特征
图 1-4 显示了与之前的实体框架(左侧区域)相比,实体框架核心(右侧区域)包含了一些新的特性。有些特性包含在实体框架 6.x 中,但不包含在实体框架核心 1.x/2.0 中。Microsoft 将在即将发布的 Entity Framework Core 版本中升级其中的一些功能,但不再添加新功能。
Note
如果你从这本书的网站上下载图片,你将能够根据颜色区分图片中的产品。
图 1-4
Functional scope of the classic Entity Framework compared to Entity Framework Core. On the left, a balloon shows some features that have been permanently eliminated.
已经取消的功能
传统实体框架中的以下功能已在实体框架核心中删除:
- 取消了流程模型数据库优先和模型优先。在 Entity Framework Core 中,只有基于代码的建模(以前称为 Code First),通过它,您可以从数据库生成程序代码(逆向工程),也可以从程序代码生成数据库(正向工程)。
- 实体数据模型(EDM)和 XML 表示(EDMX)已经被取消。到目前为止,在代码优先模型中,还在 RAM 内部生成了一个 EDM。这种开销也被消除了。
- 实体框架上下文的基类
ObjectContext
已被删除。只有基类DbContext. DbContext
现在不再是实体框架核心中ObjectContext
的包装器,而是一个全新的独立实现。 - 实体类的基类
EntityObject
已经被删除。实体类现在总是普通的旧 CLR 对象(POCOs)。 - 省略了查询语言实体 SQL (ESQL)。它支持 LINQ、SQL、存储过程(SPs)和表值函数(TVFs)。
- 不再提供自动模式迁移。模式迁移,包括数据库模式的创建,现在必须在开发时手动执行。在运行时,第一次访问数据库时,迁移仍然会发生。
- 过去有一些表和类型之间更复杂映射的场景。这包括每个类型的多个实体集(MEST,将不同的表映射到同一个实体),以及继承层次结构中的按层次结构表(TPH)、按类型表(TPT)和按具体类型表(TPC)策略的组合。所有这些功能都已删除。
缺少关键功能
在实体框架核心路线图( https://github.com/aspnet/EntityFramework/wiki/Roadmap
)中,微软开发人员 Rowan Miller 记录了即将升级的实体框架核心中缺少哪些功能。这并没有一个具体的时间表,但微软称其中一些功能是“关键的”
- 实体框架核心仅支持对表的访问,不支持对数据库中视图的访问。只有当您手动创建视图和程序代码并将视图视为表格时,您才能使用视图。
- 以前,存储过程只能用于查询数据(
SELECT
),而不能用于插入(INSERT
)、更新(UPDATE
)和删除(DELETE
)。 - 一些 LINQ 命令目前在 RAM 中执行,而不是在数据库中。这还包括
group by
操作符,这意味着数据库中的所有数据集都被读入 RAM 并在那里分组,这导致所有表的灾难性性能(除了非常小的表)。 - 实体框架核心 API 中既没有自动延迟加载,也没有显式重载。目前,开发人员只能直接加载链接的数据集(急切加载)或用单独的命令重新加载。
- 直接 SQL 和存储过程只有在返回实体类型时才能使用。还不支持其他类型。
- 现有数据库的反向工程只能从命令行或从 Visual Studio 中的 NuGet 控制台启动。基于 GUI 的向导消失了。
- 对于现有数据库,也没有“从数据库更新模型”命令;换句话说,在对数据库进行反向工程后,开发人员必须在对象模型中手动添加数据库模式更改,或者重新生成整个对象模型。这个函数在 Code First 中也不可用,只在 Database First 中可用。
- 没有复杂的类型,换句话说,类不代表它们自己的实体,而是另一个实体的一部分。
Preview
其中一些特性将在 2.1 版本中添加;参见附录 C 。
高优先级功能
在第二份清单中,微软称其他不认为重要的功能为“高优先级”:
- 到目前为止,还没有对象模型的图形可视化,这在以前的 EDMX 中是可能的。
- 一些以前存在的类型转换,比如 XML 和字符串之间的转换,现在还不存在。
- 尚不支持 Microsoft SQL Server 的地理和几何数据类型。
- 实体框架核心不支持 N:M 映射。到目前为止,开发人员必须用两个 1:N 映射和一个类似于数据库中中间表的中间实体来复制这一点。
- 尚不支持将每种类型的表作为继承策略。如果基类有一个
DBSet<T>
,实体框架核心使用 TPH 否则,它使用 TPC。您不能显式配置 TPC。 - 作为迁移的一部分,不可能用数据填充数据库(
seed()
功能)。 - 允许实体框架软件开发者在数据库中执行之前和之后操纵发送到数据库的命令的实体框架 6.0 命令拦截器还不存在。
微软高优先级列表中的一些项目也是 Entity Framework 6.x 本身(还)不掌握的新特性。
- 定义在快速加载中加载数据记录的条件(快速加载规则)
- 支持电子标签
- 支持非关系数据存储(NoSQL ),如 Azure 表存储和 Redis
这种优先顺序是从微软的角度来看的。根据我的实践经验,我会对一些要点进行不同的优先排序;例如,我会将 N:M 映射升级到 critical。通过对象模型中的两个 1:N 关系复制 N:M 是可能的,但是会使程序代码更加复杂。从现有的实体框架解决方案迁移到实体框架核心变得很困难。
这也适用于缺乏对每类型表继承的支持。同样,现有的程序代码必须进行大范围的修改。即使对于具有新数据库模式和前向工程的新应用,也存在一个问题:如果继承首先是通过 TPH 或 TPC 实现的,那么如果您以后想在 TPH 上下注,就必须费力地重新安排数据库模式中的数据。
微软的列表中还缺少验证实体等功能,当 RAM 中已经清楚实体不满足所需条件时,这些功能可以节省不必要的数据库往返。
Preview
其中一些特性将在 2.1 版本中添加;参见附录 C 。
实体框架核心中的新特性
实体框架核心相对于其前身具有以下优势:
- Entity Framework Core 不仅可以在 Windows、Linux 和 macOS 上运行,还可以在运行 Windows 10、iOS 和 Android 的移动设备上运行。在移动设备上,当然只提供对本地数据库(比如 SQLite)的访问。
- 实体框架核心提供了更快的执行速度,尤其是在读取数据时(几乎与手动将数据从
DataReader
对象复制到类型化对象的性能相同。NET 对象)。 - 带有
Select()
的投影现在可以直接映射到实体类。匿名绕道。NET 对象不再是必要的。 - 批处理允许实体框架核心将
INSERT
、DELETE
和UPDATE
操作合并到一个数据库管理系统往返中,而不是一次发送一个命令。 - 反向工程和正向工程现在都支持数据库中列的默认值。
- 除了经典的自动增量值之外,现在还允许使用序列等新方法来生成密钥。
- 实体框架核心中的术语影子属性指的是现在可能对数据库表的列的访问,对于这些列,类中没有属性。
何时使用实体框架核心
考虑到这一长串缺失的特性,问题就出现了:是否以及何时可以使用 1.x/2.0 版中的实体框架核心。
主要的应用领域是目前还没有运行 Entity Framework 的平台:Windows Phone/Mobile、Android、iOS、Linux 和 macOS。
- UWP 应用和 Xamarin 应用只能使用实体框架核心。经典的实体框架在这里是不可能的。
- 如果您想开发一个新的 ASP.NET 核心 web 应用或 web API,并且不希望它基于完整的。但在. NET Framework 上。NET 核心,没有办法用实体框架核心做到这一点,因为经典的实体框架 6.x 不能在。NET 核心。但是,在 ASP.NET 核心中,也可以使用。NET Framework 4.6.x/4.7.x 作为基础,这样你也可以使用 Entity Framework 6.x。
- 另一个推荐在 web 服务器上使用 Entity Framework Core 的场景是离线场景,在这种情况下,移动设备上应该有服务器数据库的本地副本。在这种情况下,您可以在客户机和服务器上使用相同的数据访问代码。客户端使用实体框架核心来访问 SQLite,web 服务器使用相同的实体框架核心代码来访问 Microsoft SQL Server。
对于其他平台上的项目,请注意以下几点:
- 将现有代码从 Entity Framework 6.x 迁移到 Entity Framework 核心是非常昂贵的。重要的是要考虑实体框架核心的改进特性和性能是否值得付出努力。
- 然而,在新的项目中,开发人员已经可以使用实体框架核心作为高性能的未来技术,并且如果必要的话,使用现有的实体框架作为现有差距的中间解决方案。
二、安装实体框架核心
实体框架核心没有setup.exe
。实体框架核心通过 NuGet 包安装在项目中。
NuGet 包
与经典的实体框架相比,实体框架核心由几个 NuGet 包组成。表 2-1 只显示了根包。NuGet 会自动将相关的包添加到它们的依赖项中,这里没有列出。如果您使用的是 ASP.NET 核心 Web 应用模板这样的项目模板,那么您可能已经包含了一些依赖项。
表 2-1
Main Packages Available on nuget.org for Entity Framework Core
| 数据库管理系统 | 运行时需要 NuGet 包 | 在开发阶段获取逆向工程或模式迁移所需的包 | | :-- | :-- | :-- | | Microsoft SQL Server Express,Standard,Enterprise,Developer,LocalDB(从 2008 版开始) | `Microsoft.EntityFrameworkCore.SqlServer` | `Microsoft.EntityFrameworkCore.Tools` `Microsoft.EntityFrameworkCore.SqlServer`(针对 EF 核心 2.0) `Microsoft.EntityFrameworkCore.SQLServer.Design`(针对 EF 核心 1.x) | | Microsoft SQL Server Compact 3.5 | `EntityFrameworkCore.SqlServerCompact35` | 无法使用 | | Microsoft SQL Server Compact 4.0 | `EntityFrameworkCore.SqlServerCompact40` | 无法使用 | | 数据库 | `Microsoft.EntityFrameworkCore.Sqlite` | `Microsoft.EntityFrameworkCore.Tools` `Microsoft.EntityFrameworkCore.Sqlite`(针对 EF 核心 2.0) `Microsoft.EntityFrameworkCore.Sqlite.Design`(针对 EF 核心 1.x) | | 内存中 | `Microsoft.EntityFrameworkCore.InMemory` | 无法使用 | | 一种数据库系统 | `Npgsql.EntityFrameworkCore.PostgreSQL` | `Microsoft.EntityFrameworkCore.Tools` `Npgsql.EntityFrameworkCore.PostgreSQL`(针对 EF 核心 2.0) `Npgsql.EntityFrameworkCore.PostgreSQL.Design`(针对 EF 核心 1.x) | | 关系型数据库 | `MySQL.Data.EntityFrameworkCore` | `MySQL.Data.EntityFrameworkCore.Design` | | Oracle (Devart 提供商) | `Devart.Data.Oracle.EFCore` | `Microsoft.EntityFrameworkCore.Tools` |在 Entity Framework Core 版本中,微软改变了包的剪裁(就像在 alpha 和 beta 版本中所做的那样),如图 2-1 所示。以前,每个车手都有两个包,其中一个名字里有 design。“设计”包被分解并集成到实际的驱动程序组件中。
图 2-1
In Entity Framework Core 2.0, Microsoft has integrated the classes of Microsoft.Entity FrameworkCore.SQL Server.Design.dll into Microsoft.EntityFrameworkCore.SqlServer.dll.
安装软件包
可以用 NuGet 包管理器(图 2-2 和 2-3 和 2-4 )或者 Visual Studio 中的 PowerShell cmdlet Install-Package
来安装这些包(图 2-5 和 2-6 )。
图 2-2
Installing the driver for Microsoft SQL Server with the NuGet Package Manager GUI
在命令行中(选择 NuGet 软件包管理器控制台➤ PMC),您可以安装当前的稳定版本以及与以下内容相关的依赖项:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
您可以安装当前的预发行版本,包括:
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Pre
您可以安装包含以下内容的特定版本:
图 2-5
Installing the Microsoft SQL Server driver with the NuGet Package Manager Console (shown here in version 1.1.2)
图 2-4
Installing Entity Framework Core 2.0 in a .NET Core 2.0 application includes a different set of dependencies
图 2-3
Installing Entity Framework Core 1.1.2 in a .NET Core 1.1 console
Install-Package Microsoft.EntityFrameworkCore.SqlServer version 2.0.0
您可以在 NuGet 软件包管理器控制台中使用以下命令列出软件包的所有可用版本:
(Find-Package Microsoft.Entity FrameworkCore.SqlServer -ExactMatch -allversions -includeprerelease).Versions | Format-Table Version, Release
您可以查看当前解决方案的项目中引用的包的版本,如下所示:
图 2-6
The Get-Package cmdlet shows that some projects have already been upgraded to Entity Framework Core 2.0, others not yet
(Get-Package Microsoft.EntityFrameworkCore.SqlServer) | Format-Table Projectname, id, Versions
更新到新版本
现有的项目通过 NuGet 包管理器升级到 Entity Framework Core 的新版本,无论是在图形化版本中还是在命令行中。
NuGet 包管理器 GUI 表明,当一个新的实体框架核心版本可用时,许多 NuGet 包将被更新。
Tip
由于 NuGet 包管理器有时会与许多更新“纠缠在一起”,你不应该一次更新所有的包,如图 2-7 所示。你应该只更新实际的根包,换句话说,就是带有期望的实体框架核心驱动的包(例如Microsoft.EntityFrameworkCore.SqlServer
,如图 2-8 所示)。此软件包更新还需要更新其依赖项。
图 2-7
Graphical update of all NuGet packages (not recommended!)
图 2-8
It is better to choose only the root packages, in other words, the package with the database driver
这与命令行上的过程相对应,在命令行上,您不想键入所有包,而只想更新根包,例如,升级到 Entity Framework Core 2.0 时:
Update-Package Microsoft.EntityFrameworkCore.SqlServer version 2.0.0
Tip
如果您收到错误消息“无法安装软件包。' EntityFrameworkCore . SQL server 2 . 0 . 0 '。您正在尝试将此包安装到以“”为目标的项目中。NETFramework,Version=v4.x,但该包不包含任何与该框架兼容的程序集引用或内容文件,这可能有以下原因:
图 2-9
Error message when updating to Entity Framework Core 2.0
- 您正在使用与不兼容的 4.6.1 之前的. NET 版本。NET Standard 2.0,因此无法使用实体框架核心 2.0。
- 但是,如果错误消息中的版本号是 4.6.1 或更高(见图 2-9 ),这是因为您使用的 Visual Studio 版本太旧。Entity Framework Core 2.0 只能从 Visual Studio 2015 Update 3 开始与一起使用。安装了网络核心。(就算用经典的。NET 框架,。NET Core 必须安装在开发系统上!)
Tip
从 Entity Framework Core 1.x 升级到版本 2.0 时,您需要手动删除对Microsoft.EntityFrameworkCore.SQLServer.Design
的引用。
uninstall-package Microsoft.EntityFrameworkCore.SqlServer.Design
如果你也有一个包的参考Microsoft.EntityFrameworkCore.Relational.Design
,然后删除它(图 2-10 ):
uninstall-package Microsoft.EntityFrameworkCore.Relational.Design
在 Entity Framework Core 2.0 中,微软已经将带有后缀.Design
的 NuGet 包的内容移到了没有这个后缀的同名包中。
如果您仍然有名为Microsoft.AspNetCore...
的包,即使您没有使用基于 ASP.NET 核心的 web 应用,您也可以删除它们。这些参考是实体框架核心工具的第一个版本的遗留物:
图 2-10
Uninstalling the package Microsoft.EntityFrameworkCore.SqlServer.Design, which is no longer required in Entity Framework Core 2.0
uninstall-package Microsoft.AspNetCore.Hosting.Abstractions
uninstall-package Microsoft.AspNetCore.Hosting.Server.Abstractions
uninstall-package Microsoft.AspNetCore.Http.Abstractions
uninstall-package Microsoft.AspNetCore.Http.Feature
uninstall-package System.Text.Encodings.Web
Tip
有时 Visual Studio 会在一次更新后找不到同一解决方案中其他项目的编译输出(见图 2-11 )。在这种情况下,在参考管理器中短暂停用该项目(参考➤添加参考),然后直接再次选择它(见图 2-12 )。
图 2-11
The project exists, but the compilation is not found
图 2-12
Removing and re-inserting the reference Tip
在. NET 标准库中,只有在设置为时,才能安装 Entity Framework Core 2.0。NET Standard 2.0 作为目标框架。否则,您将看到以下错误:“打包 Microsoft。EntityFrameworkCore . SQL server 2 . 0 . 0 与 netstandard1.6 不兼容(。NETStandard,版本=v1.6)。打包微软。EntityFrameworkCore . SQL server 2 . 0 . 0 支持:netstandard2.0(。NET 标准,版本=v2.0)。”同样,Entity Framework Core 2.0 不能在. NET Core 1.x 项目中使用,只能在中使用。NET Core 2.0 项目。项目可能需要提前升级(见图 2-13 和图 2-14 )。
图 2-13
Updating the target framework to .NET Standard version 2.0 in the project settings
图 2-14
Updating the target framework to .NET Core version 2.0 in the project settings
三、实体框架核心的概念
在这一章中,你将学习实体框架核心的核心概念,根据实体框架核心的过程模型和工件进行分解。
实体框架核心的过程模型
实体框架核心支持以下内容:
- 现有数据库的反向工程(从现有数据库模式创建对象模型)
- 数据库的正向工程(从对象模型生成数据库模式)。
如果您已经有了一个数据库,或者开发人员选择以传统方式创建一个数据库,那么逆向工程(通常称为数据库优先)非常有用。第二个选项称为正向工程,它使开发人员能够设计对象模型。由此,开发人员可以生成一个数据库模式。
对于开发人员来说,正向工程通常更好,因为您可以设计编程所需的对象模型。
正向工程可以在开发时(通过所谓的模式迁移)或运行时使用。模式迁移是用初始模式或后来的模式扩展/修改来创建数据库。
在运行时,这意味着当基于实体框架核心的应用运行时,数据库被创建(EnsureCreated()
)或更新(Migrate()
)。
逆向工程总是发生在开发过程中。
实体框架核心的前身 ADO.NET 实体框架支持四种流程模型,如下所示:
- 逆向工程与 EDMX 文件(又名数据库第一)
- 代码优先的逆向工程
- 用 EDMX 文件进行正向工程(又名模型优先)
- 代码优先的正向工程
因为实体框架核心中没有 EDMX,所以其中两个模型已经被淘汰。实体框架核心中的逆向工程和正向工程是相应的代码优先实践的继承者。然而,微软不再先谈代码,因为这个名字对许多开发人员来说意味着向前工程。微软一般参考基于代码的建模。图 3-1 表示正向工程和反向工程。
图 3-1
Forward engineering versus reverse engineering for Entity Framework Core
表 3-1 比较了实体框架核心中两种流程模型的特点。
表 3-1
Forward Engineering vs. Reverse Engineering in Entity Framework Core
| 特征 | 逆向工程 | 正向工程 | | :-- | :-- | :-- | | 导入现有数据库 | -好的 | 一千 | | 数据库模式的更改和扩展 | ✖(微软)公司(与第三方工具实体开发商合作) | (迁移) | | 图形模型 | ✖(微软)公司(与第三方工具实体开发商合作) | ✖(微软)公司(与第三方工具实体开发商合作) | | 存储过程 | 可通过第三方工具实体开发人员手动使用(微软)映射代码生成 | 可手动使用(微软) | | 表值函数 | 可通过第三方工具实体开发人员手动使用(微软)映射代码生成 | 可手动使用(微软) | | 视图 | 可通过第三方工具实体开发人员手动使用(微软)映射代码生成 | 一千 | | 在对象模型中拥有元数据/注释 | 有可能,但不容易使用第三方工具实体开发人员更容易 | 非常简单! | | 对对象设计的控制 | 一千 | -好的 | | 清楚 | 一千 | -好的 |实体框架核心的组件
图 3-2 说明了一个实体框架核心项目的关键组件以及它们与传统数据库对象的关系。
图 3-2
The central artifacts in Entity Framework Core and their context
数据库管理系统(DBMS)包含一个带有表和视图的数据库。
Note
目前只能使用带有主键的表或带有包含主键的视图的表。在 Entity Framework Core 版本 2.1 中,将有读取(但不改变)没有主键的表的选项(参见附录 C )。
实体类(也称为域对象类、业务对象类、数据类或持久类)是表和视图的表示。它们包含映射到表/视图列的属性或字段。实体类可以是普通的旧 CLR 对象(POCO 类);换句话说,它们不需要基类和接口。但是,您不能仅使用这些对象来访问数据库。
Best Practice
虽然可以使用字段,但是您应该只使用属性,因为许多其他库和框架都需要属性。
上下文类是一个总是从DbContext
基类派生的类。对于每个实体类,它都有类型为DbSet<EntityClass>
的属性。上下文类或DbSet
属性以 LINQ 命令、SQL 命令、存储过程和表值函数(TVF)调用或用于追加、修改和删除的特殊 API 调用的形式获取自创建程序代码的命令。context 类将命令发送给特定于 DBMS 的提供者,后者通过DbCommand
对象将命令发送给数据库,并从数据库接收DataReader
中的结果集。上下文类将DataReader
对象的内容转换成实体类的实例。这个过程叫做物化。
四、现有数据库的逆向工程(数据库优先开发)
本章讨论现有数据库的反向工程。反向工程是指从现有的数据库模式中创建对象模型。
本章介绍了更简单的版本 1 的万维网 Wings 数据库模式。您可以用 SQL 脚本WWWings66.sql
安装这个数据库模式,它也提供数据(10,000 次航班和 200 名飞行员)。
使用逆向工程工具
这个过程没有现成的可视化工具,但是微软未来的版本可能会包含一些选项。在第十九章,我将介绍一些额外的工具来帮助你完成这个过程,如下所述:
- Visual Studio 开发环境中 NuGet 包管理器控制台(PMC)的 PowerShell cmdlets。这些命令不仅可以在中使用。净核心项目还要在“全”。NET 框架项目。
- 命令行。NET Core 工具(Windows 中称为
dotnet.exe
),也可以独立于 Visual Studio 和 Windows 使用。但是,这仅适用于。NET Core 或基于 ASP.NET Core 的项目。
使用 PowerShell Cmdlets 进行反向工程
对于逆向工程,实体框架核心 2.0 中有两个相关的 NuGet 包。
- 在 Visual Studio 的当前启动项目中,开发时需要包
Microsoft.EntityFrameworkCore.Tools
。 - 在生成程序代码的项目中和在带有工具的项目中,需要每个实体框架核心数据库驱动程序的包(例如,
Microsoft.EntityFrameworkCore.SqlServer
或Microsoft.EntityFrameworkCore.Sqlite
)。
Tip
虽然理论上可以只使用一个项目并包含两个包,但实际上您应该为实体框架核心工具创建自己的项目。目前,只使用实体框架核心工具。启动项目已完成,但仍未使用。或者,也可以在程序代码生成后卸载实体框架核心工具。这可以让你的项目更干净、更集中。
对于本章中的示例,继续创建这两个项目:
EFC_Tools.csproj
只为工具而存在。此项目将是一个. NET Framework 控制台应用(EXE)。- 程序代码在
EFC_WWWWingsV1_Reverse.csproj
中生成。该项目是一个. NET 标准 2.0 库,因此可以在。NET 框架以及。NET Core、Mono 和 Xamarin。
在这种情况下,您必须首先安装软件包,因此在EFC_Tools.csproj
中执行以下操作:
Install-Package
Microsoft.EntityFrameworkCore.Tools Install-package Microsoft.EntityFrameworkCore.SqlServer
在EFC_WWWingsV1_Reverse.csproj
中执行以下操作:
Install-package Microsoft.EntityFrameworkCore.SqlServer
或者对 SQLite 执行以下操作:
Install-package Microsoft.EntityFrameworkCore.Sqlite
Note
在 Entity Framework Core 1.x 中,还必须将包Microsoft.EntityFrameworkCore.SqlServer.Design
或Microsoft.EntityFrameworkCore.Sqlite.Design
包含在工具项目中。在实体框架核心 2.0 中不再需要这些包。
当您运行工具的包安装命令时,总共 33 个程序集引用将被添加到一个. NET 4.7 项目中(参见图 4-1 )。在 Entity Framework Core 1.x 中,甚至有更多,包括 ASP.NET 核心程序集,即使你根本不在 ASP.NET 核心项目中。
图 4-1
The Microsoft.EntityFrameworkCore.Tools package will add 33 more packages!
如果在没有安装软件包的情况下执行代码生成命令,开发人员会在软件包管理器控制台中看到一个错误(参见图 4-2 )。
图 4-2
Scaffold-DbContext without previous package installation
生成代码
然后,在通过Scaffold-DbContext
cmdlet 安装了两个包之后,运行实际的代码生成,开发人员至少需要将数据库提供者的名称和一个连接字符串传递给 cmdlet。
Scaffold-DbContext -Connection "Server=DBServer02;Database=WWWings;Trusted_Connection=True;MultipleActiveResultSets=True;" -Provider Microsoft.EntityFrameworkCore.SqlServer
这个命令为在 NuGet 包管理器控制台中设置为当前目标项目的项目中的这个数据库中的所有表创建类。对于无法映射的数据库列,Scaffold-DbContext
发出警告(见图 4-3 )。
图 4-3
Scaffold-DbContext warns that a column of type Geography has been ignored
或者,可以使用一个模式或表来限制生成特定的数据库模式名或表名。对于这两个参数,您可以指定几个用分号分隔的名称。
Scaffold-DbContext -Connection "Server=DBServer02;Database=WWWWingsV1;Trusted_Connection=True;MultipleActiveResultSets=True;" -Provider Microsoft.EntityFrameworkCore.SqlServer -Tables Flight,Person,Pilot,Passenger,Airport,Employee,Flight_Passenger -force
您可以使用带有或不带有模式名的表名(换句话说,您可以使用Flight
或operation.Flight
)。但是要注意,如果一个同名的表存在于多个模式中,那么没有模式名的规范将为所有模式中同名的所有表生成实体类。
默认情况下,代码是在 NuGet 包管理器控制台中当前选择的项目的根目录中生成的,使用的是该项目的默认名称空间。通过参数-Project
和-OutputDir
,开发者可以影响项目和输出文件夹。不幸的是,使用现有的参数,不可能将实体类和上下文类的代码生成指向不同的项目。
关于图 4-4 所示的数据模型,Scaffold-DbContext
cmdlet 现在生成以下输出:
图 4-4
Example database for the Word Wide Wings airline (version 1)
- 为六个表中的每一个生成一个 POCO 样式的实体类,包括 N:M 中间表
Flight_Passenger
,它总是消除对象模型中的经典实体框架。遗憾的是,实体框架核心 2.0 版还不支持 N:M 关系;它只是用两个 1:N 关系来复制它们,关系模型也是如此。 - 由基类
Microsoft.EntityFrameworkCore.DbContext
派生的上下文类被派生。与以前不同,这个类不再是ObjectContext
类的包装器,而是一个全新的独立实现。这个类的名字可以被开发者用命令行参数-Context
影响。不幸的是,在这里不可能指定名称空间。Visual Studio 通过“传入的上下文类名不是有效的 C# 标识符”来确认在参数值中使用点。 - 如果不能为单个列生成代码,那么在包管理器控制台中将会有一个黄色的警告输出。例如,
Geometry
和Geography
数据类型会发生这种情况,因为还不支持实体框架核心。
图 4-5 显示了从图 4-4 中为样本数据库生成的类,图 4-6 显示了对象模型。
图 4-6
Object model of the generated classes
图 4-5
Project with the generated classes for the sample database from Figure 4-4
不幸的是,您不能给实体框架核心的代码生成器任何名称空间的设置;对于生成的实体类和上下文类,它总是使用项目的默认命名空间。因此,您应该在生成项目中设置默认的名称空间,以便它至少适合实体类。然后,您只需手动更改上下文类的名称空间。
与 ADO.NET 实体框架的逆向工程不同,实体框架核心不会自动将连接字符串包含在应用配置文件app.config
或web.config
中。连接字符串在生成后位于上下文类的OnConfiguring()
方法中,由软件开发人员为其找到合适的可能的备份位置。
多元化(换句话说,将类名中的表名改为复数)不会发生。到目前为止,实体框架核心中还没有选项可以做到这一点;然而,这并不是一个很大的损失,因为多元化服务只适用于英文表名。
查看生成的程序代码
下面的清单显示了由Scaffold-DbContext
为上下文类生成的程序代码,例如,为实体类Flight
和Passenger
生成的程序代码。
对象模型如何映射到数据库模式的定义在传统的实体框架中以三种方式继续:
- 实体框架核心自动应用的约定
- 由实体类及其成员应用的数据注释
- 在
DbContext
类的OnModelCreating()
方法中使用的 Fluent API
实体框架核心工具生成的代码侧重于第三种方式。上下文类中的OnModelCreating()
方法相应地充满了流畅的 API 调用。但是约定继续起作用,例如,标准中的类的属性与表中的列具有相同的名称。
到目前为止,Visual Studio 中经典实体框架的助手还使用了数据注释,现在已不再用于生成的程序代码中。如果想找回旧的行为,可以使用Scaffold-DbContext
中的参数-DataAnnotations
。
Fluent API 包含以下定义:
- 它使用
ToTable()
定义数据库中的表名,如果它们不同或者它们的模式名不同于dbo
。 - 它使用
HasName()
定义主键列和索引的名称。 - 它定义列类型和列属性。NET 类型名称对于数据库管理系统中的一个数据类型来说不是唯一的,使用
HasColumnType()
、IsRequired()
、HasMaxLength()
。 - 它使用
HasDefaultValueSql()
或HasDefaultValue()
定义列的默认值。 - 它使用
HasOne()
、HasMany()
、WithOne()
、WithMany()
、HasForeignKey()
和HasConstraintName()
来定义表和它们的外键之间的基数。 - 它使用
HasIndex()
定义索引。 - 它决定在插入或修改实体框架核心记录后是否必须重新读取列的内容,因为它是由数据库管理系统使用
ValueGeneratedOnAddOrUpdate()
、ValueGeneratedOnAdd()
和ValueGeneratedNever()
生成的。 - 它使用
OnDelete()
设置级联删除设置。
在源代码中,Fluent API 配置是按类组织的,如下所示:
modelBuilder.Entity<Person>(entity => {...});
在这些方法调用中,您将找到这些表的各个列的配置。
entity.Property(e => e.PersonId)...
与以前的 ADO.NET 实体框架相比,有一些语法上的变化和改进。因此,索引配置现在更加简洁了。
以下特性就像经典的实体框架:
- 逆向工程代码生成器不会在实体类之间创建继承关系,即使这在乘客➤人、雇员➤人和飞行员➤人的情况下是可能的。相反,代码生成器总是生成关联和关联的导航属性。这种继承关系必须由开发人员稍后定义,然后移除导航属性。
- 例如,实体类中的导航属性被声明为
virtual
,即使实体框架核心延迟加载还不支持virtual
所必需的。 - 例如,集合的导航属性用
ICollection<T>
声明,然后用新的HashSet<T>()
填充到构造函数中。
- 例如,实体类中的导航属性被声明为
- 对于每个实体类,在上下文类中都有一个
DbSet<T>
属性。
可以更改生成的源代码(参见清单 4-1 ,清单 4-2 ,清单 4-3 ),例如,如果您想在对象模型的数据库中拥有除列名之外的属性名。您可以使用 Fluent API 方法HasColumnName("column name")
或数据注释Column("column name")
来完成这项工作。
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace EFC_WWWingsV1_Reverse
{
public partial class WWWingsV1Context : DbContext
{
public virtual DbSet<Airport> Airport { get; set; }
public virtual DbSet<Employee> Employee { get; set; }
public virtual DbSet<Flight> Flight { get; set; }
public virtual DbSet<FlightPassenger> FlightPassenger { get; set; }
public virtual DbSet<Metadaten> Metadaten { get; set; }
public virtual DbSet<MigrationHistory> MigrationHistory { get; set; }
public virtual DbSet<Passenger> Passenger { get; set; }
public virtual DbSet<Person> Person { get; set; }
public virtual DbSet<Pilot> Pilot { get; set; }
public virtual DbSet<Protokoll> Protokoll { get; set; }
public virtual DbSet<Test> Test { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
optionsBuilder.UseSqlServer(@"Server=.;Database=WWWingsV1;Trusted_Connection=True;MultipleActiveResultSets=True;");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Airport>(entity =>
{
entity.HasKey(e => e.Name);
entity.ToTable("Airport", "Properties");
entity.Property(e => e.Name)
.HasColumnType("nchar(30)")
.ValueGeneratedNever();
});
modelBuilder.Entity<Employee>(entity =>
{
entity.HasKey(e => e.PersonId);
entity.ToTable("Employee", "People");
entity.Property(e => e.PersonId)
.HasColumnName("PersonID")
.ValueGeneratedNever();
entity.Property(e => e.HireDate).HasColumnType("datetime");
entity.Property(e => e.SupervisorPersonId).HasColumnName("Supervisor_PersonID");
entity.HasOne(d => d.Person)
.WithOne(p => p.Employee)
.HasForeignKey<Employee>(d => d.PersonId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_MI_Employee_PE_Person");
entity.HasOne(d => d.SupervisorPerson)
.WithMany(p => p.InverseSupervisorPerson)
.HasForeignKey(d => d.SupervisorPersonId)
.HasConstraintName("FK_Employee_Employee");
});
modelBuilder.Entity<Flight>(entity =>
{
entity.HasKey(e => e.FlightNo);
entity.ToTable("Flight", "Operation");
entity.Property(e => e.FlightNo).ValueGeneratedNever();
entity.Property(e => e.Airline).HasMaxLength(3);
entity.Property(e => e.Departure)
.IsRequired()
.HasMaxLength(30);
entity.Property(e => e.Destination)
.IsRequired()
.HasMaxLength(30);
entity.Property(e => e.FlightDate).HasColumnType("datetime");
entity.Property(e => e.Memo).IsUnicode(false);
entity.Property(e => e.PilotPersonId).HasColumnName("Pilot_PersonID");
entity.Property(e => e.Timestamp).IsRowVersion();
entity.Property(e => e.Utilization).HasColumnName("Utilization ");
entity.HasOne(d => d.PilotPerson)
.WithMany(p => p.Flight)
.HasForeignKey(d => d.PilotPersonId)
.HasConstraintName("FK_FL_Flight_PI_Pilot");
});
modelBuilder.Entity<FlightPassenger>(entity =>
{
entity.HasKey(e => new { e.FlightFlightNo, e.PassengerPersonId })
.ForSqlServerIsClustered(false);
entity.ToTable("Flight_Passenger", "Operation");
entity.Property(e => e.FlightFlightNo).HasColumnName("Flight_FlightNo");
entity.Property(e => e.PassengerPersonId).HasColumnName("Passenger_PersonID");
entity.HasOne(d => d.FlightFlightNoNavigation)
.WithMany(p => p.FlightPassenger)
.HasForeignKey(d => d.FlightFlightNo)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Flight_Passenger_Flight");
entity.HasOne(d => d.PassengerPerson)
.WithMany(p => p.FlightPassenger)
.HasForeignKey(d => d.PassengerPersonId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Flight_Passenger_Passenger");
});
modelBuilder.Entity<Passenger>(entity =>
{
entity.HasKey(e => e.PersonId);
entity.ToTable("Passenger", "People");
entity.Property(e => e.PersonId)
.HasColumnName("PersonID")
.ValueGeneratedNever();
entity.Property(e => e.CustomerSince).HasColumnType("datetime");
entity.Property(e => e.PassengerStatus).HasColumnType("nchar(1)");
entity.HasOne(d => d.Person)
.WithOne(p => p.Passenger)
.HasForeignKey<Passenger>(d => d.PersonId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PS_Passenger_PE_Person");
});
modelBuilder.Entity<Person>(entity =>
{
entity.ToTable("Person", "People");
entity.Property(e => e.PersonId).HasColumnName("PersonID")
;
entity.Property(e => e.Birthday).HasColumnType("datetime");
entity.Property(e => e.City).HasMaxLength(30);
entity.Property(e => e.Country).HasMaxLength(2);
entity.Property(e => e.Email)
.HasColumnName("EMail")
.HasMaxLength(50);
entity.Property(e => e.GivenName)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.Memo).IsUnicode(false);
entity.Property(e => e.Surname)
.IsRequired()
.HasMaxLength(50);
});
modelBuilder.Entity<Pilot>(entity =>
{
entity.HasKey(e => e.PersonId);
entity.ToTable("Pilot", "People");
entity.Property(e => e.PersonId)
.HasColumnName("PersonID")
.ValueGeneratedNever();
entity.Property(e => e.FlightSchool).HasMaxLength(50);
entity.Property(e => e.Flightscheintyp).HasColumnType("nchar(1)");
entity.Property(e => e.LicenseDate).HasColumnType("datetime");
entity.HasOne(d => d.Person)
.WithOne(p => p.Pilot)
.HasForeignKey<Pilot>(d => d.PersonId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PI_Pilot_MI_Employee");
});
}
}
}
Listing 4-1Generated Context Class
using System;
using System.Collections.Generic;
namespace EFC_WWWingsV1_Reverse
{
public partial class Flight
{
public Flight()
{
FlightPassenger = new HashSet<FlightPassenger>();
}
public int FlightNo { get; set; }
public string Airline { get; set; }
public string Departure { get; set; }
public string Destination { get; set; }
public DateTime FlightDate { get; set; }
public bool NonSmokingFlight { get; set; }
public short Seats { get; set; }
public short? FreeSeats { get; set; }
public int? PilotPersonId { get; set; }
public string Memo { get; set; }
public bool? Strikebound { get; set; }
public int? Utilization { get; set; }
public byte[] Timestamp { get; set; }
public Pilot PilotPerson { get; set; }
public ICollection<FlightPassenger> FlightPassenger { get; set; }
}
}
Listing 4-2Generated Entity Class Flight
using System;
using System.Collections.Generic;
namespace EFC_WWWingsV1_Reverse
{
public partial class Passenger
{
public Passenger()
{
FlightPassenger = new HashSet<FlightPassenger>();
}
public int PersonId { get; set; }
public DateTime? CustomerSince { get; set; }
public string PassengerStatus { get; set; }
public Person { get; set; }
public ICollection<FlightPassenger> FlightPassenger { get; set; }
}
}
Listing 4-3Generated Entity Class Passenger
查看示例客户端
清单 4-4 中显示的程序使用了生成的实体框架上下文类和实体类Passenger
。
所示的方法创建了一个新乘客,将该乘客附加到DbSet<Passenger>
,然后使用SaveChanges()
方法将新乘客存储在数据库中。
然后所有的乘客都要接受检查,他们的号码被打印出来。清单 4-4 显示了名为 Schwichtenberg 的所有乘客的版本。然后在 RAM 中对先前装载的乘客上方的对象进行 LINQ 过滤。图 4-7 在屏幕上显示输出。
Note
本例中使用的命令在本书后面的章节中会有更详细的描述。然而,这个清单对于证明所创建的实体框架核心上下文类的功能是必要的。
图 4-7
Output of the sample client
public static void Run()
{
Console.WriteLine("Start...");
using (var ctx = new WWWingsV1Context())
{
// Create Person object
var newPerson = new Person();
newPerson.GivenName = "Holger";
newPerson.Surname = "Schwichtenberg";
// Create Passenger object
var newPassenger = new Passenger();
newPassenger.PassengerStatus = "A";
newPassenger.Person = newPerson;
// Add Passenger to Context
ctx.Passenger.Add(newPassenger);
// Save objects
var count = ctx.SaveChanges();
Console.WriteLine("Number of changes: " + count);
// Get all passengers from the database
var passengerSet = ctx.Passenger.Include(x => x.Person).ToList();
Console.WriteLine("Number of passengers: " + passengerSet.Count);
// Filter with LINQ-to-Objects
foreach (var p in passengerSet.Where(x=>x.Person.Surname == "Schwichtenberg").ToList())
{
Console.WriteLine(p.PersonId + ": " + p.Person.GivenName + " " + p.Person.Surname);
}
}
Console.WriteLine("Done!");
Console.ReadLine();
}
Listing 4-4Program Code That Uses the Created Entity Framework Core Model
使用。网络核心工具 dotnet
发展的时候。NET 核心项目的命令行工具dotnet
(也称为。NET 核心命令行界面[CLI])。NET Core SDK 可以作为 PowerShell cmdlet(https://www.microsoft.com/net/download/core
)的替代方案。与 PowerShell cmdlets 不同,dotnet
不仅适用于 Windows,也适用于 Linux 和 macOS。
这种形式的生成适用于以下情况:
- 。NET 核心控制台应用
- ASP.NET 核心项目基于。NET Core 或。NET Framework 4.6.2 和更高版本
首先,必须安装包Microsoft.EntityFrameworkCore.Tools.DotNet
,这不能通过命令行工具,只能通过在基于 XML 的.csproj
项目文件中手动输入(见图 4-8 ):
图 4-8
Manual extension of the .csproj file
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
</ItemGroup>
然后,您必须添加以下包:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.1" />
</ItemGroup>
但是,这也可以通过项目目录中的命令行来实现,如下所示:
dotnet add package Microsoft.EntityFrameworkCore.design
现在添加所需的实体框架核心提供者,如下所示:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
以下软件包在 Entity Framework Core 1.x 中也是必需的,但在 Entity Framework Core 2.0 中不再需要:
dotnet add package Microsoft.EntityFrameworkCore.SQL Server.design
然后就可以进行代码生成了(见图 4-9 )。
图 4-9
Reverse engineering with dotnet.exe
dotnet ef dbcontext scaffold "server =.; Database = WWWings66; Trusted_Connection = True; MultipleActiveResultSets = True; "Microsoft.EntityFrameworkCore.SqlServer --output-dir model
Note
微软直到 2017 年 3 月 6 日才发布dotnet.exe
1.0 的最终版本,作为实体框架 Core 1.1.1 和 Visual Studio 2017 的一部分。以前,只有“预览”版本。这些预览版本使用了一个project.json
文件。如果你仍然使用这种过时的格式,你不必在.csproj
文件中做条目;你得在project.json
档里做!
了解逆向工程的弱点
与传统的实体框架一样,您只能为带有主键的表创建实体类型。然而,复合主键对于实体框架核心来说不是问题。
Note
微软将在 2.1 版本中引入没有主键的表的映射;更多信息见附录 C 。
对于 SQL Server 2016 中添加的时态表(称为系统版本化表),历史表不能使用实体框架核心进行映射。但是,对于实际的表来说,这已经是可能的了,因此只能通过 SQL 查询历史值,目前还不能通过 LINQ。
对于数据库视图和存储过程,与经典的实体框架相反,不能生成类和函数。
一旦使用实体框架核心命令行工具生成了对象模型,就不能对其进行更新。可用于“数据库优先”方法的“从数据库更新模型”命令目前尚未实现。您只能重新开始生成。如果要生成的类已经存在,cmdlet Scaffold-DbContext
会报错。使用附加参数-force
,cmdlet 将覆盖现有文件。但是,对源代码文件的任何手动更改都将丢失。
如果在一个新的Scaffold-DbContext
命令中,您没有生成所有先前生成的表,而是只生成了几个选定的表,那么在上下文类中,所有现在不再生成的表都缺少DbSet<T>
声明和 Fluent API 配置。同样,这也是生成一个项目的原因,然后您可以从该项目中将所需的生成部分复制到另一个项目中。然而,微软已经宣布(在 https://github.com/aspnet/EntityFramework/wiki/Roadmap
)它计划改进工具,并提供一个从数据库特性更新的模型。
在此之前,这是最好的方法,至少可以限制在更改新表时产生的代码;对新的、已更改的或已删除的列的更改最好在源代码中手动完成。或者,在一个数据库的逆向工程之后,可以切换到正向工程;在这种情况下,更改将被记录在对象模型中,并用于生成更改数据库模式的 DDL 命令。
五、新数据库的正向工程
尽管 Entity Framework Core 支持现有数据库模型的逆向工程,但理想的流程模型是正向工程,其中数据库模型是从对象模型生成的。这是因为开发人员可以根据业务案例的需要来设计对象模型。
正向工程在经典的实体框架中有两种变体:模型优先和代码优先。在 Model First 中,您以图形方式创建一个实体数据模型(EDM)来生成数据库模式和。NET 类。在代码优先中,您直接编写类,从这些类中创建数据库模式。电火花是看不见的。在重新设计的实体框架核心中,只有第二种方法,但是不叫代码优先,而是基于代码的建模,不再使用不可见的 EDM。
两种类型的课程
实体框架核心中基于代码的建模通过以下两种类型的类实现:
- 您创建实体类,将数据存储在 RAM 中。您在实体类中创建导航属性,这些属性表示实体类之间的关系。这些通常是普通的旧 CRL 对象(POCOs ),每个数据库列都有属性。
- 您编写了一个表示数据库模型的上下文类(从
DbContext
派生而来),每个实体都被列为一个DBSet
。这将用于所有查询和其他操作。
理想情况下,这两种类型的类在不同的项目(DLL 程序集)中实现,因为实体类经常在软件架构的几层甚至所有层中使用,而上下文类是数据访问层的一部分,应该只由它上面的层使用。
本章中的示例
本章展示了如何在版本 2 中创建一个初步版本的 World Wide Wings 对象模型。最初,您将只考虑实体Person
、Employee
、Pilot
、Passenger
、Flight
和Booking
。您将只设置从对象模型创建数据库模式所需的最少选项。您将在接下来的章节中扩展和细化对象模型。你可以在解决方案EFC_WWWings
中找到程序代码。实体类位于名为EFC_BO_Step1
(用于业务对象)的 DLL 项目中,上下文类位于名为EFC_DA_Step1
(用于数据访问)的 DLL 项目中。启动应用是控制台应用(EFC_Konsole
)。这包括数据访问代码和屏幕输出。见图 5-1 。
Note
为了保持示例的简单性并专注于使用实体框架核心 API,我没有进一步描述业务逻辑,也没有在上下文类之上创建专用的数据访问层。这不是一个架构上的例子;这些类型的例子将在本书的后面介绍。
图 5-1
Solution for the example in this and the following chapters
自行创建实体类的规则
如前所述,实体类是 POCOs。换句话说,它们不必从基类继承或实现接口。但是,必须有一个无参数的构造函数,当数据库表行被具体化时,实体框架核心可以使用它来创建实例。
Note
本章最初只描述了实体类的典型基本配置。你会在第十二章中找到改编版本。
NuGet 包
您不需要引用任何实体框架核心 NuGet 包来实现实体类。然而,使用数据注释如[Key]
和[StringLength]
需要参考经典中的System.ComponentModel.Annotations.dll
。NET 框架或者 NuGet 包System.ComponentModel.Annotations
( https://www.nuget.org/packages/System.ComponentModel.Annotations
)中。NET 核心和。净标准。
数据注释属性
要在数据库表中创建的每一列都必须由一个属性表示。这些属性可以是带有{get; set;}
的自动属性,也可以是带有 getter 和 setter 实现的显式属性(参见Flight
类中的属性Memo
)。一个类也可以拥有字段;但是,这些字段不会映射到列。换句话说,默认情况下,这些字段中的信息是不持久的。此外,没有 setter 的属性也不会被持久化,比如类Person
中的属性Fullname
。
数据类型
那个。允许使用网络原始数据类型(String
、DateTime
、Boolean
、Byte
、Int16
、Int32
、Int64
、Single
、Double
、Decimal
、System.Guid
)。Nullable<T>
可以指示数据库表格中的相应列可以留空(NULL
)。也允许枚举类型;例如,参见Pilot
类中的PilotLicenseType
。数据类型DbGeometry
和DbGeography
,从版本 5.0 开始在经典的实体框架中被支持,不幸的是现在在实体框架核心中不存在。
关系(大纲-细节)
也可能有被声明为不同实体类型的属性。这些被称为导航属性,它们表达了两个实体类之间的关系。实体框架核心支持以下内容:
- 1:0/1 关系:在这里,属性被声明为相关类型的单个对象(参见
Flight
类中的Pilot
和Copilot
)。
Important
对于单个对象,在导航属性声明或构造函数中分配相关类型的实例在语义上是错误的,因为或映射器(如实体框架核心)会看到新的实体对象。只有当一个新的顶层对象总是需要一个新的子对象时,这种实例化才有意义。在Flight
和Pilot
的情况下,情况并非如此,因为没有为每次飞行设置新的飞行员。
- 1:0 / N 关系:在这里,属性被声明为相关类型的集合类型(参见
List<Flight>
中的FlightAsPilotSet
和Pilot
类中的FlightAsCopilotSet
)。允许将导航属性声明为ICollection
或基于它的任何其他接口(如IList
)或声明为ICollection<T>
类(如List<T>
或HashSet<T>
)。
Important
在声明或构造函数中直接指定具体的集合类型通常是一个好主意,这样调用程序代码就不必这样做了。实体框架核心处理关系修正中的集合实例化。因为这里只创建了一个空列表,所以只要列表没有被填充,实体框架核心就不想在这里保存任何东西。
Note
实体框架核心当前不支持 N:M 关系。更具体地说,Entity Framework Core 不支持用对象模型中的一个中间表作为 N:M 来表示两个 1:N 关系的抽象,关系数据库模型也不支持 N:M 关系;它需要一个用于两个 1:n 关系的中间表,其中中间表包含来自要连接的实体的主键的复合主键。在 Entity Framework Core 中,对象模型中的中间表有一个中间类,就像在关系模型中一样,有两个 1:N 关系,而不是 N:M 关系。这样的中间实体类可以在类预订中看到。它被类别Flight
和类别Passenger
中的预订集引用。
在经典的实体框架核心中,导航属性必须被标记为virtual
,这样延迟加载才能工作。由于实体框架 Core 1.x/2.0 中没有惰性加载,所以不再需要这个版本。延迟加载将在实体框架核心的更高版本中出现;到目前为止,还不清楚微软是否会再次要求将此作为标签。但是现在将导航属性声明为virtual
并无大碍。
导航属性可以是双向的,如实体类Flight
和Pilot
所示。导航属性的 0/1 页可以具有(但不需要)显式外键属性(参见PilotId
和CopilotId
)。
继承
实体类可以相互继承。这可以在类Pilot
和Passenger
中看到,它们继承自类Person
。在本例中,Person
是一个抽象类。但是它也可以是具有实例的类。
主关键字
实体框架核心的另一个先决条件是每个实体类必须有一个由一个或多个简单属性组成的主键(PK)。在最简单的情况下,根据惯例,您创建一个名为ID
或Id
或classnameID
或classnameId
的属性。ID 和类名的大小写敏感性在这里是不相关的,即使在 www.efproject.net/en/latest/modeling/keys.html
的实体框架核心仍然不完整的文档有不同的提示。
Note
如果你想重命名主键或者定义一个复合主键,那么你必须显式地配置它,你将在第十二章学到。
例子
清单 5-1 、 5-2 、 5-3 、 5-4 、 5-5 和 5-6 2 反映了部分 World Wide Wings 实例类。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BO
{
[Serializable]
public class Flight
{
public Flight()
{ }
#region Key
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)] // No identity column!
public int FlightNo { get; set; }
#endregion
#region Primitive Properties
[StringLength(50), MinLength(3)]
public string Departure { get; set; }
[StringLength(50), MinLength(3)]
public string Destination { get; set; }
[Column("FlightDate", Order = 1)]
public DateTime Date { get; set; }
public bool? NonSmokingFlight { get; set; }
[Required]
public short? Seats { get; set; }
public short? FreeSeats { get; set; }
public decimal? Price { get; set; }
public string Memo { get; set; }
#endregion
#region Related Objects
public Airline { get; set; }
public ICollection<Booking> BookingSet { get; set; }
public Pilot { get; set; }
public Pilot Copilot { get; set; }
// Explicit foreign key properties for the navigation properties
public string AirlineCode { get; set; } // mandatory!
public int PilotId { get; set; } // mandatory!
public int? CopilotId { get; set; } // optional
public byte? AircraftTypeID { get; set; } // optional
#endregion
public override string ToString()
{
return String.Format($"Flight #{this.FlightNo}: from {this.Departure} to {this.Destination} on {this.Date:dd.MM.yy HH:mm}: {this.FreeSeats} free Seats.");
}
public string ToShortString()
{
return String.Format($"Flight #{this.FlightNo}: {this.Departure}->{this.Destination} {this.Date:dd.MM.yy HH:mm}: {this.FreeSeats} free Seats.");
}
}
}
Listing 5-1Class Flight
using System;
namespace BO
{
public class Person
{
#region Primitive properties
// --- Primary Key
public int PersonID { get; set; }
// --- Additional properties
public string Surname { get; set; }
public string GivenName { get; set; }
public Nullable<DateTime> Birthday { get; set; }
public virtual string EMail { get; set; }
// --- Relations
public Persondetail Detail { get; set; } = new Persondetail(); // mandatory (no FK property!)
#endregion
// Calculated property (in RAM only)
public string FullName => this.GivenName + " " + this.Surname;
public override string ToString()
{
return "#" + this.PersonID + ": " + this.FullName;
}
}
}
Listing 5-2Class Person
using System;
namespace BO
{
public class Employee : Person
{
public DateTime? HireDate;
public float Salary { get; set; }
public Employee Supervisor { get; set; }
public string PassportNumber => this._passportNumber;
private string _passportNumber;
public void SetPassportNumber(string passportNumber)
{
this._passportNumber = passportNumber
;
}
}
}
Listing 5-3Class Employee
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
public enum PilotLicenseType
{
// https://en.wikipedia.org/wiki/Pilot_licensing_and_certification
Student, Sport, Recreational, Private, Commercial, FlightInstructor, ATP
}
[Serializable]
public partial class Pilot : Employee
{
// PK ist inherited from Employee
#region Primitive
public virtual DateTime LicenseDate { get; set; }
public virtual Nullable<int> FlightHours { get; set; }
public virtual PilotLicenseType
{
get;
set;
}
[StringLength(50)]
public virtual string FlightSchool
{
get;
set;
}
#endregion
#region Related Objects
public virtual ICollection<Flight> FlightAsPilotSet { get; set; }
public virtual ICollection<Flight> FlightAsCopilotSet { get; set; }
#endregion
}
}
Listing 5-4Class Pilot
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
public class PassengerStatus
{
public const string A = "A";
public const string B = "B";
public const string C = "C";
public static string[] PassengerStatusSet = { PassengerStatus.A, PassengerStatus.B, PassengerStatus.C };
}
[Serializable]
public partial class Passenger : Person
{
public Passenger()
{
this.BookingSet = new List<Booking>();
}
// Primary key is inherited!
#region Primitive Properties
public virtual Nullable<DateTime> CustomerSince { get; set; }
[StringLength(1), MinLength(1), RegularExpression("[ABC]")]
public virtual string Status { get; set; }
#endregion
#region Relations
public virtual ICollection<Booking> BookingSet { get; set; }
#endregion
}
}
Listing 5-5Class Passenger
namespace BO
{
/// <summary>
/// Join class for join table
/// </summary>
public class Booking
{
// Composite Key: [Key] not possible, see Fluent API!
public int FlightNo { get; set; }
// Composite Key: [Key] not possible, see Fluent API!
public int PassengerID { get; set; }
public Flight { get; set; }
public Passenger { get; set; }
}
}
Listing 5-6Class Booking
自创建上下文类的规则
context 类是实体框架核心编程的关键,在实现它时需要遵循一些规则。
Note
本章仅描述了上下文类的典型基本配置。你会在第十二章中找到改编版本。
安装 NuGet 包
对于 context 类的实现,你需要一个 NuGet 包,用于你各自的数据库管理系统(见表 5-1 )。例如,在 NuGet 软件包管理器控制台中输入以下内容:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
对于 SQLite,输入以下内容:
Install-Package Microsoft.EntityFrameworkCore.Sqlite
对于 Oracle,输入以下内容:
Install-Package Devart.Data.Oracle.EFCore
在传统的实体框架中,只需要引用两个程序集(并且这些引用必须手动创建),而新的 NuGet 包(在核心产品模块化的意义上)包含了 32 个混杂的引用(参见项目DAL
),这是您不希望手动创建的。对于项目BO
,不需要引用实体框架 DLL!
表 5-1
The Entity Framework Core Providers Available on nuget.org
| 数据库管理系统 | NuGet 包 | | :-- | :-- | | Microsoft SQL Server Express,标准版,企业版,开发版,LocalDB 2008+ | `Microsoft.EntityFrameworkCore.SqlServer` | | Microsoft SQL Server Compact 3.5 | `EntityFrameworkCore.SqlServerCompact35` | | Microsoft SQL Server Compact 4.0 | `EntityFrameworkCore.SqlServerCompact40` | | 数据库 | `Microsoft.EntityFrameworkCore.sqlite` | | 一种数据库系统 | `Npgsql.EntityFrameworkCore.PostgreSQL` | | 在内存中(用于单元测试) | `Microsoft.EntityFrameworkCore.InMemory` | | 关系型数据库 | `MySQL.Data.EntityFrameworkCore` | | Oracle (DevArt) | `Devart.Data.Oracle.EFCore` |基础类
上下文类不是 POCO 类。它必须从基类Microsoft.EntityFrameworkCore.DbContext
继承。曾经存在于经典实体框架中的备选基类ObjectContext
已经不存在了。
构造器
context 类必须有一个无参数的构造函数才能在 Visual Studio 或命令行中使用架构迁移工具,因为这些工具必须在设计时实例化 context 类。如果在应用启动时专门生成数据库模式,则不需要无参数构造函数。然后开发人员就有机会用构造函数参数调用上下文类。
Note
如果没有显式的构造函数,C# 会自动拥有一个无参数的构造函数。
对实体类的引用
开发人员必须为每个实体类创建一个类型为DbSet<EntityType>
的属性,如下所示:
public DbSet<Flight> FlightSet {get; set; }
public DbSet<Pilot> PilotSet {get; set; }
Caution
默认情况下,实体框架核心使用此处显示的属性名作为数据库模式中的表名。你将在以后学习如何改变这种行为。
提供程序和连接字符串
要寻址的数据库的连接字符串必须在经典实体框架中通过构造函数传递给基类DbContext
的本地实现。实体框架核心有一个不同的方法,即一个叫做OnConfiguring()
的新方法,它必须被覆盖。该方法由实体框架核心调用,用于流程中上下文的第一次实例化。方法OnConfiguring()
接收一个DbContextOptionsBuilder
的实例作为参数。在OnConfiguring()
中,然后调用DbContextOptionsBuilder
实例上的扩展方法,该方法确定数据库提供者和连接字符串。要调用的扩展方法由实体框架核心数据库提供者提供。在 Microsoft SQL Server 的情况下,它被命名为UseSqlServer()
,并期望连接字符串作为参数。将连接字符串移动到合适的位置(例如,配置文件)并从那里加载取决于您。
Note
虽然在代码中包含一个连接字符串对于实际项目来说是一个糟糕的做法,但这是让示例变得清晰的最佳解决方案。因此,本书中的许多清单将连接字符串保存在代码中。在实际项目中,您应该从配置文件中读取连接字符串。
外包配置数据的能力在很大程度上取决于项目的类型,像这里展示的解决方案不能在任何其他类型的项目中运行。对各种配置系统和相关 API 的处理不是本书的一部分。请参考上的基本文档。. NET。NET Core、UWP 和 Xamarin。
连接字符串必须包含MultipleActiveResultSets = True
,否则实体框架核心在某些情况下可能无法正常工作;您将得到以下错误消息:“已经有一个打开的 DataReader 与此命令相关联,必须先将其关闭。”
builder.UseSqlServer(@"Server=MyServer;Database=MyDatabase;Trusted_Connection=True;MultipleActiveResultSets=True");
Attention
如果在OnConfiguring()
中没有调用UseXY()
方法,那么将出现以下运行时错误:“没有为此 DbContext 配置数据库提供程序。可以通过重写 DbContext 来配置提供程序。OnConfiguring 方法或通过在应用服务提供者上使用 AddDbContext。如果使用 AddDbContext,那么还要确保您的 DbContext 类型在其构造函数中接受 DbContext options
看到一个例子
清单 5-7 显示了基本配置中的 World Wide Wings 示例的上下文类。
using BO;
using Microsoft.EntityFrameworkCore;
namespace DA
{
/// <summary>
/// EFCore context class for World Wings Wings database schema version 7.0
/// </summary>
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<Booking> BookingSet { get; set; }
#endregion
public static string ConnectionString { get; set; } =
@"Server=.;Database=WWWingsV2_EN_Step1;Trusted_Connection=True;MultipleActiveResultSets=True;App=Entityframework";
public WWWingsContext() { }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer(ConnectionString);
}
}
Listing 5-7Context Class
你自己的关系
UseSqlServer()
和其他驱动程序也可以接收连接对象(类DbConnection
的一个实例)而不是连接字符串。不一定要事先打开连接。可以打开它,然后使用现有的连接。实体框架核心上下文也不关闭它。如果没有打开,实体框架核心上下文将根据需要打开和关闭连接。
Best Practice
基本上,你应该远离实体框架核心连接管理!只有在非常特殊的情况下(例如,跨多个上下文实例的事务),您才应该自己打开连接!
线程安全
DbContext
类不是线程安全的,这意味着从DbContext
继承的自己创建的上下文类在任何情况下都不能在几个不同的线程中使用。每个线程都需要自己的上下文类实例!忽视这一点的人会冒在实体框架核心中出现不可预测的行为和奇怪的运行时错误的风险!对于那些使用依赖注入的人来说,DbContext
应该作为一个Transient
对象。
Note
当然,这也适用于使用逆向工程生成的DbContext
类。
数据库模式生成规则
实体框架代码然后从实体类和能够存储实体类的所有实例的上下文类生成数据库模式。数据库模式的结构基于约定和配置。这里适用先约定后配置的原则。
有许多惯例。以下是最重要的:
- 从每个实体类(在上下文类中有一个
DbSet<T>
)创建一个表。在经典的实体框架中,标准系统中实体类的类名是复数。在实体框架核心中,标准现在使用上下文类中的DbSet<T>
属性的名称。 - 实体类中的每个基本属性都成为表中的一列。
- 名为
ID
的属性或名为ID
的类自动成为具有自动增量值的主键。 - 对于导航属性的每 1/0 边,都会创建一个额外的外键列,即使没有显式的外键属性。
- 被命名为导航属性加上后缀
ID
的属性表示自动生成的外键列。 - 枚举类型成为数据库中的
int
列。
Note
虽然在许多情况下,这些约定足以从对象模型创建数据库模式,但在这种情况下,这些约定是不够的。不幸的是,在编译时您不会发现这一点;只有在执行使用 context 类的程序代码时,您才会发现它。
请看一个示例客户端
清单 5-8 中的程序现在使用创建的实体框架上下文类和实体类Passenger
。首先,通过调用EnsureCreated()
方法,程序确保数据库被创建,如果它还不存在的话。传统实体框架中已知的数据库初始化类不再存在于实体框架核心中。
此后,程序创建一个新乘客,将该乘客附加到DbSet<Passenger>
,然后使用SaveChanges()
方法将新乘客存储在数据库中。
然后所有的乘客都被装载,他们的号码被打印出来。最后,一个版本的所有乘客的名字 Schwichtenberg 如下。这种过滤随后在 RAM 中进行,并对先前装载的乘客上方的物体进行 LINQ。
Note
本例中使用的命令在本书后面的章节中会有更详细的描述。然而,这个讨论对于证明所创建的实体框架核心上下文类的功能是必要的。
不幸的是,这个例子还不能无错运行。在下一章,你将了解为什么会这样,以及如何解决这些问题。
using DA;
using BO;
using System;
using System.Linq;
namespace EFC_Console
{
class SampleClientForward
{
public static void Run()
{
Console.WriteLine("Start...");
using (var ctx = new WWWingsContext())
{
// Create database at runtime, if not available!
var e = ctx.Database.EnsureCreated();
if (e) Console.WriteLine("Database has been created!");
// Create passenger object
var newPassenger = new Passenger();
newPassenger.GivenName = "Holger";
newPassenger.Surname = "Schwichtenberg";
// Append Passenger to EFC context
ctx.PassengerSet.Add(newPassenger);
// Save object
var count = ctx.SaveChanges();
Console.WriteLine("Number of changes: " + count);
// Read all passengers from the database
var passengerSet = ctx.PassengerSet.ToList();
Console.WriteLine("Number of passengers: " + passengerSet.Count);
// Filter with LINQ-to-Objects
foreach (var p in passengerSet.Where(x => x.Surname == "Schwichtenberg").ToList())
{
Console.WriteLine(p.PersonID + ": " + p.GivenName + " " + p.Surname);
}
}
Console.WriteLine("Done!");
Console.ReadLine();
}
}
Listing 5-8Program Code That Uses the Created Entity Framework Core Model
通过 Fluent API(onmodelcreasing())进行适配
当您启动清单 5-8 中的程序代码时,EnsureCreated()
方法首先会遇到以下运行时错误:“无法确定导航属性 Flight 所表示的关系。“Pilot”类型的“Pilot”。请手动配置该关系,或者使用“[NotMapped]”特性或使用“EntityTypeBuilder”忽略此属性。“OnModelCreating”中的“Ignore”。"
这样实体框架核心告诉你,在Flight
和Pilot
(带有属性Pilot
和Copilot
)双向关系的情况下,它不知道Pilot
侧的两个导航属性(FlightsAsPilotSet
和FlightAsCopilotSet
)中的哪一个对应于Flight
侧的导航属性Pilot
和Copilot
。
为了澄清这一点,在实体框架核心中有所谓的 Fluent API,它在经典的实体框架中首先在代码中可用。Fluent API 由方法protected override void OnModelCreating
( ModelBuilder modelBuilder
)组成,该方法将在上下文类中被覆盖。在modelBuilder
对象上,然后在方法的调用链中进行配置。
protected override void OnModelCreating(ModelBuilder builder)
{
...
}
在Pilot
和Flight
是双向关系的情况下,在OnModelCreating()
中要输入以下两个方法链,其中飞行员明确关联FlightAsPilot
,而Copilot
关联FlightAsCopilot
:
modelBuilder.Entity<Pilot>().HasMany(p => p.FlightAsPilotSet).WithOne(p => p.Pilot).HasForeignKey(f => f.PilotId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Pilot>().HasMany(p => p.FlightAsCopilotSet).WithOne(p => p.Copilot).HasForeignKey(f => f.CopilotId).OnDelete(DeleteBehavior.Restrict);
使用.OnDelete(DeleteBehavior.Restrict)
,您关闭了级联删除,这在这种情况下没有意义。
如果您随后再次启动程序,您会得到运行时错误“实体类型”BO。“预订”需要定义一个密钥。实体框架核心不知道中间类 booking 中的主键是什么,因为按照约定,那里没有可以作为主键的属性。应该有一个复合主键。因此,您必须在 Fluent API 中添加以下内容:
modelBuilder.Entity<Booking>().HasKey(b => new { b.FlightNo, b.PassengerID });
实体框架仍然不满意,并在程序下一次启动时抱怨类Flight
、Passenger
和staff
中缺少主键。在Flight
中,这一点很清楚,因为FlightNr
不符合约定(即FlightID
)。因此,添加以下内容:
modelBuilder.Entity<Flight>().HasKey(x => x.FlightNo);
然而,乘客和雇员继承基类Person
的主键PersonID
。不幸的是,实体框架核心不够聪明,没有注意到这一点。所以也要补充一下。
modelBuilder.Entity<Employee>().HasKey(x => x.PersonID);
modelBuilder.Entity<Passenger>().HasKey(x => x.PersonID);
这样,程序代码终于可以执行了!
现在问题来了,为什么实体框架 Core 不抱怨Pilot
没有主键。这是因为实体框架核心在数据库中映射继承的方式。飞行员不是存储在单独的表中,而是与雇员存储在同一个表中。因此,实体框架核心不会为飞行员抱怨。
清单 5-9 显示了上下文类的改进版本。有了这个版本,程序现在可以按预期执行了。图 5-2 显示输出。
图 5-2
Output of the sample client using the improved context class (in a .NET Core console app)
using BO;
using Microsoft.EntityFrameworkCore;
namespace DA
{
/// <summary>
/// EFCore context class for World Wings Wings database schema version 7.0
/// </summary>
public class WWWingsContext : DbContext
{
#region Tables
public DbSet<Flight> FlightSet { get; set; }
public DbSet<Passenger> PassengerSet { get; set; }
public DbSet<Pilot> PilotSet { get; set; }
public DbSet<Booking> BookingSet { get; set; }
#endregion
public static string ConnectionString { get; set; } =
@"Server=.;Database=WWWingsV2_EN_Step1;Trusted_Connection=True;MultipleActiveResultSets=True;App=Entityframework";
public WWWingsContext() { }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer(ConnectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region Configure the double relation between Flight and Pilot
// fix for problem: "Unable to determine the relationship represented by navigation property Flight.Pilot' of type 'Pilot'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'."
modelBuilder.Entity<Pilot>().HasMany(p => p.FlightAsPilotSet).WithOne(p => p.Pilot).HasForeignKey(f => f.PilotId).OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Pilot>().HasMany(p => p.FlightAsCopilotSet).WithOne(p => p.Copilot).HasForeignKey(f => f.CopilotId).OnDelete(DeleteBehavior.Restrict);
#endregion
#region Composite key for BookingSet
// fix for problem: 'The entity type 'Booking' requires a primary key to be defined.'
modelBuilder.Entity<Booking>().HasKey(b => new { FlightNo = b.FlightNo, PassengerID = b.PassengerID });
#endregion
#region Other Primary Keys
// fix for problem: 'The entity type 'Employee' requires a primary key to be defined.'
modelBuilder.Entity<Employee>().HasKey(x => x.PersonID);
// fix for problem: 'The entity type 'Flight' requires a primary key to be defined.'
modelBuilder.Entity<Flight>().HasKey(x => x.FlightNo);
// fix for problem: 'The entity type 'Passenger' requires a primary key to be defined.'
modelBuilder.Entity<Passenger>().HasKey(x => x.PersonID);
#endregion
base.OnModelCreating(modelBuilder);
}
}
}
Listing 5-9Improved Version of the Context Class
查看生成的数据库模式
图 5-3 显示了生成的数据库。
图 5-3
The resulting database model
如您所见,实体框架核心仅从六个实体类(Person
、Employee
、Pilot
、Passenger
、Flight
和Booking
)中生成数据库中的四个表。实体框架核心混合了每个具体类型的继承映射策略表(TPC)和每个层次的继承映射策略表(TPH)。
- 没有表
Person
。抽象实体类Person
的所有属性已经被移动到Passenger
和Employee
表中。 - 根据 TPH 原则,表
Employee
还包括实体类Pilot
的实例。有一个列Discriminator
,它自动用值Pilot
或Employee
填充实体框架核心。
到目前为止,使用实体框架核心的开发人员对继承映射策略的影响非常小。你只能间接影响 TPC 和 TPH 之间的决策。如果在 context 类中有一个DbSet<Person>
和DbSet<Employee>
,那么实体框架核心将完全应用层次表(TPH)策略,换句话说,从Person
、Employee
、Pilot
和Passenger
中只生成一个表。那么就不需要在 Fluent API 中为员工和乘客制定明确的键规范!
Note
实体框架核心中尚不存在按类型分类的表(TPT)继承映射策略。
与经典的实体框架一样,实体框架核心为所有主键和外键创建索引。与经典的实体框架一样,实体框架核心将标准中的字符串列设置为nvarchar(max)
。这个还是需要调整的。比经典实体框架更好的是,实体框架核心创建数据类型为DateTime2(7)
的日期列,而不是像以前那样创建数据类型为DateTime
的日期列。因此,问题不再在于。SQL Server 1 . 1 . 1601 版之前拒绝净有效日期。
六、定制数据库模式
在许多情况下,实体框架核心可以在正向工程中仅基于来自对象模型的约定来创建数据库模式。然而,前一章已经表明,约定并不总是足以创建有效的数据库模式。计算机需要软件开发人员的指导来创建复合主键,创建用于继承类的主键,以及停用级联删除;否则,数据库会导致循环删除操作。
在其他情况下,尽管实体框架核心可以创建数据库模式,但结果并不令人满意。这两种情况在前一章中都有介绍(参见表名和字符串列的长度)。
在本章中,您将学习如何通过实体类中的数据注释或通过OnModelCreating()
方法中的 Fluent API 显式配置来覆盖或补充约定。
本章中的示例
虽然前一章使用了 World Wide Wings 对象模型版本 2 的初级阶段,但本书现在将涵盖完整的对象模型版本 2。你会在项目EFC_GO
、EFC_DA
、EFC_Console
中的项目文件夹EFC_WWWings
中找到程序代码(见图 6-1 )。
图 6-1
Solution EFC_WWWings
惯例与配置
有两种方法可以在实体框架核心中配置数据库模式。
- 实体类中的数据注释
- 在 context 类的
OnModelCreating()
方法中使用 Fluent API
这里列出了三个基本规则:
- 通过数据注释或 Fluent API 进行的配置比约定更重要。换句话说,配置会覆盖个别情况下的约定。微软使用“配置前的约定”来谈论实体框架核心然而,这意味着目标是按照惯例尽可能地使显式配置变得多余。
- 当存在冲突的数据注释和流畅的 API 调用时,流畅的 API 调用总是胜出。
- 您可以通过 Fluent API 表达所有配置选项。其中的一部分也可以通过数据注释来实现。
持久类与瞬态类
持久类是来自对象模型的类,其实例存储在数据库中。在实体框架核心中,持久类也称为实体类。相比之下,瞬态类只有完全位于主存中的易变实例。
基本上每个。NET 类是瞬态的。如果下列任一项为真,则实体框架核心使类持久化:
- 上下文类中有一个
DbSet<EntityClass>
- 在 Fluent API 中有一个对
modelBuilder.Entity<EntityClass>()
的调用 - 另一个持久类通过导航属性引用这个类
使用第二个选项通常没有意义,因为没有DbSet<EntityClass>
或导航属性,通过实体框架核心的数据访问类就不可用。
如果一个持久类与一个瞬态类有关系,开发人员有时可能希望定义与第三条规则的偏差。在这种情况下,开发人员必须用 Fluent API 中的[NotMapped]
或modelBuilder.Ignore<Class>()
来注释要保持瞬态的相关类。
如果您不想在数据库中持久化持久化类的单个属性,也可以在类的属性级别使用[NotMapped]
,因为默认情况下,实体框架核心持久化具有 getter 和 setter 的实体类的所有属性。Fluent API 为此使用了Ignore()
方法,但这一次它是在调用Entity<T>(): modelBuilder.Entity<EntityClass>().Ignore(x => x.Property)
之后这样做的。
特别是,如果实体类属性具有更复杂的。实体框架核心无法映射的. NET 数据类型。例如,这适用于类system.Xml.XmlDocument
。实体框架核心无法生成数据库架构,并给出以下错误:“键{'TempId'}包含处于影子状态的属性,并且被从' XmlSchemaCompilationSettings '到' XmlSchemaSet '的关系引用。编译设置。为此关系配置一个非影子主体密钥。尽管在 Microsoft SQL Server 和其他数据库管理系统中有 XML 数据类型,但在实体框架核心中,有一个到。NET 类system.Xml.XmlDocument
还没有实现。
数据库模式中的名称
按照惯例,实体框架核心分配以下内容:
- 每个表都获得在
DbSet<EntityClass>
的上下文类中使用的属性名。 - 对于每个没有
DbSet <entity class>
的实体类,实体框架核心使用类名作为表名。 - 每一列都获得实体类中属性的名称。
要改变这一点,您可以使用表 6-1 中描述的选项。
表 6-1
Changing Conventionally Specified Table and Column Names in the Database Schema
| | 数据注释 | 流畅的 API | | :-- | :-- | :-- | | 表名 | 在一个类的前面:`[Table("TableName")]`或者带有模式名的附加说明:`[Table("TableName", schema = "SchemaName")]`如果没有模式名,表总是以默认模式结束,默认模式是`dbo`。 | `modelBuilder.Entity表中列的顺序
实体框架核心按如下方式对表中的列进行排序:
- 首先,主键列按字母顺序出现。
- 然后所有其他列按字母顺序出现。
- 稍后添加的列不会按顺序排序,而是添加在后面。
与经典的实体框架不同,实体框架核心不遵循源代码中属性的顺序。微软在 https://github.com/aspnet/EntityFramework/issues/2272
对此解释如下:“在 EF6 中,我们试图让列顺序与类中属性的顺序相匹配。问题是反射可能在不同的架构上返回不同的顺序。”
在经典的实体框架中,顺序可以通过注释[Column(Order = Number)]
来配置。但是,这只会影响第一次创建表时的情况,不会影响以后添加的列,因为在许多数据库管理系统中,在现有列之间对新列进行排序需要重新构建表。根据微软的说法,“没有任何方法可以做到这一点,因为 SQL Server 需要重建表(重命名现有表,创建新表,复制数据,删除旧表)来重新排序列”( https://github.com/aspnet/EntityFramework/issues/2272
)。因此,微软决定不尊重实体框架核心中注释[Column]
的Order
属性。
列类型/数据类型
. NET 类型的数据库架构中使用的数据库类型不是由实体框架核心决定的,而是由数据库提供程序决定的。例如,表 6-2 显示了在 Microsoft SQL Server、SQLite 和 DevArt Oracle provider 中默认选择的内容。
Note
虽然列类型到的映射是固定的。NET 数据类型在实体框架核心 2.0 中,微软将在实体框架核心 2.1 中引入值转换器;参见附录 C 。值转换器允许在读取或写入数据库时转换属性值。
表 6-2
Mapping .NET Data Types to Column Types
| 。网络数据类型 | Microsoft SQL Server 列类型 | SQLite 列类型 | Oracle 列类型 | | :-- | :-- | :-- | :-- | | `Byte` | `Tinyint` | `INTEGER` | `NUMBER(5, 0)` | | `Short` | `Smalintl` | `INTEGER` | `NUMBER(5, 0)` | | `Int32` | `Int` | `INTEGER` | `NUMBER(10, 0)` | | `Int64` | `Bitint` | `INTEGER` | `NUMBER(19, 0)` | | `DateTime` | `DateTime2` | `TEXT` | `TIMESTAMP(7)` | | `DateTimeOffset` | `datetimeoffset` | `TEXT` | `TIMESTAMP(7) WITH TIME ZONE` | | `TimeSpan` | `time` | `TEXT` | `INTERVAL DAY(2) TO SECOND(6)` | | `String` | `nvarchar(MAX)` | `TEXT` | `NCLOB` | | `String limited length` | `nvarchar(x)` | `TEXT` | `NVARCHAR2(x)` | | `Guid` | `Uniqueidentifier` | `BLOB` | `RAW(16)` | | `Float` | `Real` | `REAL` | `BINARY_FLOAT` | | `Double` | `Float` | `REAL` | `BINARY_DOUBLE` | | `Decimal` | `decimal(18,2)` | `TEXT` | `NUMBER` | | `Byte[]` | `varbinary(MAX)` | `BLOB` | `BLOB` | | `[Timestamp] Byte[]` | `Rowversion` | `BLOB` | `BLOB` | | `Byte` | `Tinyint` | `INTEGER` | `NUMBER(5, 0)` | | 其他数组类型,如`short[]`、`int[]`和`string[]` | 实体框架核心尚不支持映射。您将得到以下错误:“无法映射属性“xy ”,因为它的类型为“Int16[]”,这不是受支持的基元类型或有效的实体类型。请显式映射此属性,或者使用“[NotMapped]”属性或使用“EntityTypeBuilder”忽略它。“OnModelCreating”中的“Ignore”。 | | 茶 | 实体框架核心尚不支持映射。您将看到以下错误:“属性“xy”的类型为“char ”,当前数据库提供程序不支持该类型。请更改属性 CLR 类型,或者使用“[NotMapped]”特性或使用“EntityTypeBuilder”忽略该属性。“OnModelCreating”中的“Ignore”。 | 整数 | 实体框架核心尚不支持映射。您将看到以下错误:“属性“xy”的类型为“char ”,当前数据库提供程序不支持该类型。请更改属性 CLR 类型,或者使用“[NotMapped]”特性或使用“EntityTypeBuilder”忽略该属性。“OnModelCreating”中的“Ignore”。 | | 文件 | 实体框架核心尚不支持映射。您将得到以下错误:“实体类型“XmlSchemaCompilationSettings”需要定义主键。” |如果您不同意这种数据类型等效,您必须使用数据注释[Column]
或使用 Fluent API 中的HasColumnType()
。
这里有一个例子:
[Column(TypeName = "varchar(200)")]
modelBuilder.Entity<Entitätsklasse>()
.Property(x => x.Destination).HasColumnType("varchar(200)")
Caution
经典实体框架从 5.0 版本开始支持的DbGeometry
和DbGeography
类还不能在实体框架核心中使用。到目前为止,SQL Server 的Geometry
和Geography
列类型还没有映射。
必填字段和可选字段
约定声明,只有数据库中的那些列才被创建为“可空的”,其中。对象模型中的. NET 类型也可以允许空值(或在 Visual Basic 中为空)。网)。换句话说,它可以接受string
、byte[]
,以及显式的可空值类型Nullable<int>
、int?
、Nullable<DateTime>
、DateTime?
,以此类推。
使用注释[Required]
或modelBuilder.Entity<EntityClass>().Property(x => x. Propertyname).
IsRequired()
,您可以确定一个属性在数据库中不应该为空,即使该属性在您的代码中实际上允许为空或不允许任何内容。不能使用批注或 Fluent API 强制可为空的列;如果数据库允许列中有空值,但代码中相应的属性不可为空,则会出现运行时错误。
Note
因为…的行为。NET 值类型,可能有必要将一个列声明为具有[Required]
属性的int?
,以确保该值是实际提供的,而不只是设置为。净默认值为 0。
字段长度
第五章中生成的数据库模式的一个显著缺点是生成了所有长数据类型nvarchar(max)
的字符串列。默认情况下,实体框架核心将作为主键的字符串列限制为 450 个字符。
您可以用注释[MaxLength(number)]
或[StringLength(number)]
或modelBuilder.Entity<EntityClass>().Property(x => x.PropertyName).HasMaxLength(number)
定义长度限制。
主键
按照惯例,表的主键是一个名为ID
或Id
或ClassNameID
或ClassNameId
的属性。这些名字的大小写不相关。不幸的是,如果您在一个类中使用了这些变体中的一种以上(在 C# 中所有四种都是可能的,但在 Visual Basic 中只有两种是可能的。NET,因为这种语言是不区分大小写的),实体框架核心按照程序代码中的顺序获取第一个匹配的属性。与此约定相对应的所有其他属性都成为表中的普通列。
如果另一个属性要成为主键,必须用[Key]
对其进行注释,或者在 Fluent API: modelBuilder.Entity<EntityClass>().HasKey(x => x.Property)
中编写。与经典的实体框架相反,复合主键不再能够通过数据注释在实体框架核心中指定;它们只能通过 Fluent API 来指定,就像在builder.Entity<Booking>().HasKey(x => new { x.FlightNo, x.Passenger ID })
中一样。
对于整数主键(byte
、short
、int
、long
),Entity Framework Core 在数据库模式中创建默认标识列(也称为自动递增列)。文档是这样写的:“按照惯例,整数或 GUID 数据类型的主键将被设置为在 add 上生成值”( https://docs.microsoft.com/en-us/ef/core/modeling/generated-properties
)。整数是一个容易让人误解的通称(因为这个句子对于byte
、short
、long
也是成立的)。如果不希望自动增加列,请在属性前使用注释[DatabaseGenerated(DatabaseGeneratedOption.None)]
或在OnModelCreating()
中使用modelBuilder.Entity<class>().Property(x => x.PropertyName).ValueGeneratedNever()
。
关系和外键
实体框架核心自动将引用另一个实体类的一个或多个实例的属性视为导航属性。这允许您创建实体之间的关系(例如 1:1 和 1:N 的主从关系)。
对于集合,开发者可以使用ICollection
或者任何其他基于它的接口(比如IList
,也可以使用任何ICollection<T>
-实现类(比如List<T>
或者HashSet<T>
)。Entity Framework Core 自动在数据库模式的表中的以下位置创建外键列:
- 在 1:N 关系的 N 端
- 在 1:0/1 关系的一方
外键列包含导航属性的名称以及相关实体类的主键的名称。对于每个外键列,Entity Framework Core 会自动在数据库中生成一个索引。
对于清单 6-1 中的程序代码(将实体类型AircraftType
引入到 World Wide Wings 示例中),外键列AircraftTypeTypeId
在Flight
表中创建(Type
出现两次,因为它同时属于类名和主键名)。要让实体框架核心使用一个更简单的名称,您可以在导航属性上使用注释[ForeignKey("AircraftTypeId")]
。在 Fluent API 中,这有点复杂,因为在调用方法HasForeignKey()
以获得Set
外键列的名称之前,您必须用HasOne()
、HasMany()
、WithOne()
和WithMany()
显式地表示基数。
builder.Entity<Flight>().HasOne(f => f. AircraftType).WithMany(t=>t.FlightSet).HasForeignKey("AircraftTypeId");
你可以选择从哪个方向来建立这种关系。因此,下面的命令行相当于前面的命令行。在程序代码中包含这两条指令并不是错误,而是不必要的。
builder.Entity<AircraftType>().HasMany(t => t.FlightSet).WithOne(t => t. AircraftType).HasForeignKey("AircraftTypeTypeId");
这个外键列也可以通过外键属性显式映射到对象模型中(参见清单 6-1 中的public byte AircraftTypeID {get; set;}
)。但是,这种显式映射不是强制性的。
Tip
通过对象模型中的属性显示外键列的优点是,可以通过外键建立关系,而不必加载完整的相关对象。按照约定,如果某个属性与实体框架核心默认为外键列选择的名称相匹配,则实体框架核心会自动将该属性视为外键属性。
可选关系和强制关系
清单 6-1 介绍了实体类型AircraftType
和AircraftTypeDetail
。一个Aircraft
正好有一个AircraftType
,一个AircraftType
正好有一个AircraftTypeDetail
。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
/// <summary>
/// AircraftType has a dependent object AircraftTypeDetail (1:1)
/// AircraftTypeDetail uses the same primary key as AircraftType
/// </summary>
public class AircraftType
{
[Key]
public byte TypeID { get; set; }
public string Manufacturer { get; set; }
public string Name { get; set; }
// Navigation Property 1:N
public List<Flight> FlightSet { get; set; }
// Navigation Property 1:1, unidirectional, no FK Property
public AircraftTypeDetail Detail { get; set; } }
}
using System.ComponentModel.DataAnnotations;
namespace BO
{
/// <summary>
/// AircraftTypeDetail is a dependent object (1:1) of AircraftType
/// AircraftTypeDetail uses the same primary key as AircraftType
/// </summary>
public class AircraftTypeDetail
{
[Key]
public byte AircraftTypeID { get; set; }
public byte? TurbineCount { get; set; }
public float? Length { get; set; }
public short? Tare { get; set; }
public string Memo { get; set; }
public AircraftType { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EFCExtensions;
namespace BO
{
public class Flight
{
#region Key
public int FlightNo { get; set; }
#endregion
...
#region Related Objects
public ICollection<Booking> BookingSet { get; set; }
public Pilot { get; set; }
public Pilot Copilot { get; set; }
[ForeignKey("AircraftTypeID")]
public AircraftType AircraftType { get; set; }
// Explicit foreign key properties for the navigation properties
public int PilotId { get; set; } // mandatory!
public int? CopilotId { get; set; } // optional
public byte? AircraftTypeID { get; set; } // optional
#endregion
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
[Serializable]
public partial class Pilot : Employee
{
// PK is inherited from Employee
...
#region Related Objects
public virtual ICollection<Flight> FlightAsPilotSet { get; set; }
public virtual ICollection<Flight> FlightAsCopilotSet { get; set; }
#endregion
}
}
Listing 6-1New Entity Classes AircraftType and AircraftTypeDetail with the Relevant Cutouts from the Related Flight and Pilot Classes
在清单 6-1 中,Flight
和AircraftType
之间的关系是一种强制关系,即每个航班必须被分配一个AircraftType
,因为外键属性AircraftTypeID
必须被分配一个值。见图 6-2 。
要使这种关系成为可选的,即允许没有分配AircraftType
的Flight
对象,属性必须允许外键列为零或为零。在这种情况下,就必须有:public byte? Aircraft TypeNr {get; set; }
。您还可以在 Fluent API 中创建与IsRequired()
的强制关系,即使外键列允许 null 或空,如下所示:
builder.Entity<Flight>()
.HasOne(f => f.AircraftType)
.WithMany(t => t.FlightSet)
.IsRequired()
.HasForeignKey("AircraftTypeID");
Note
如果没有显式外键属性,默认情况下,该关系是可选的。同样,您需要调用方法IsRequired()
来强制强制关系。
图 6-2
Relationship between Flight and AircraftType and AircraftTypeDetail
单向和双向关系
在对象模型中,两个实体类之间的关系可以是双向的;也就是说,有双向导航属性,既有从Flight
到AircraftType
(通过类Flight
中的AircraftType
属性),也有从AircraftType
到Flight
(通过类AircraftType
中的FlightSet
属性)。或者,单向关系是允许的,因为在两个类中的一个类中简单地省略了导航(参见AircraftTypeDetail
和AircraftTypeDetail
之间的关系,这是单向的)。清单 6-1 显示了AircraftType
有一个名为Detail
的导航属性,它引用了一个AircraftTypeDetail
对象。但是在AircraftTypeDetail
的实现中,没有AircraftType
的导航属性。然而,双向关系通常是有意义的,因此对象模型更容易使用,特别是因为它们不占用数据库中的额外空间,只占用主存中的最小空间。
在双向关系中,实体框架核心使用约定来找到两个匹配的导航属性及其基数。因此,如果Flight
有一个类型为AircraftType
的导航属性,而AircraftType
有一个类型为List<Flight>
的导航属性,那么实体框架核心会自动假设 1:N 关系。
然而,这种基于约定的机制对于Flight
Pilot
关系是不可能的,因为在Flight
类中有两个类型为Pilot
(名为Pilot
和Copilot
)的导航属性,在Pilot
类中有两个类型为List<Flight>
( FlightAsPilotSet
和FlightAsCopilotSet
)的导航属性。在这一点上,你必须给实体框架核心关于什么属于一起的相关提示。这可以通过数据注释[InverseProperty("FlightAsPilotSet")]
或[InverseProperty("FlightAsCopilotSet")]
或 Fluent API 来完成,如下所示:
builder.Entity<Pilot>().HasMany(p => p.FlightAsCopilotSet)
.WithOne(p => p.Copilot).HasForeignKey(f => f.CopilotId);
builder.Entity<Pilot>().HasMany(p => p.FlightAsPilotSet)
.WithOne(p => p.Pilot).HasForeignKey(f => f.PilotId);
在 World Wide Wings 示例中,Flight
和Pilot
在导航属性Pilot
上的关系是强制关系;Copilot
是可选的。
取消副驾驶,让飞机在紧急情况下由空乘人员降落(就像 1997 年的电影《乱流》中一样)顺便提一下,这是爱尔兰瑞安航空公司的老板迈克尔·奥利里在 2010 年提出的真实建议(见 www.dailymail.co.uk/news/article-1308852/Let-stewardesses-land-plane-crisis-says-Ryanair-boss-Airline-wants-ditch-pilots.html
)。
一对一的关系
清单 6-1 还显示了AircraftType
和AircraftTypeDetail
之间的 1:1 关系。这是一种强制关系;也就是说,每个AircraftType
对象必须正好有一个AircraftTypeDetail
对象,因为类之间的关系不受外键列的支持。AircraftType
和AircraftTypeDetail
具有名称和类型相同的主键属性。因此,这种关系从AircraftType.TypeID
到AircraftTypeDetail.AircraftTypeID
产生。
AircraftType.TypeID
被创建为自动增量值。Entity Framework Core 非常聪明,它还创建了一个自动递增的值AircraftTypeDetail.AircraftTypeID
,因为这两个数字必须真正对应,这样关系才能起作用。
如果AircraftType.TypeID
不是一个自动增加的值,实体框架核心会为AircraftTypeDetail.AircraftTypeID
做一个,这会导致问题。AircraftType.TypeNr
没有自动增加的值,但是实体框架核心仍然不存储在源代码中明确分配的值。实体框架核心然后为AircraftType.TypeID
使用自动增量值,这是AircraftTypeDetail.AircraftTypeID
指定的。只有当AircraftType.TypeID
和AircraftTypeDetail.AircraftTypeID
都设置为ValueGeneratedNever()
时,您才能自由设置数值。在这里,你必须帮点忙,将AircraftTypeDetail.AircraftTypeID
配置为没有自动增量值。
builder.Entity<AircraftTypeDetail>().Property(x => x. AircraftTypeID).ValueGeneratedNever().
如果实体类AircraftTypeDetail
有不同的主键名(例如,No
,实体框架核心将创建这个主键列作为自动增值列,并在AircraftType
表中添加一个外键列(名为DetailNo
)。这种关系将是 1:0/1 的关系,所以可能有没有AircraftTypeDetail
对象的AircraftType
对象。那么你就不容易从数据中看出这种关系;例如,AircraftType
对象#456 可以与AircraftTypeDetail
对象#72 相关联。
一个DbSet<AircraftTypeDetail>
不必存在于上下文类中。另外,AircraftType
和AircraftTypeDetail
之间的关系是单向关系,因为从AircraftType
到AircraftTypeDetail
只有一种导航类型,而从AircraftTypeDetail
到AircraftType
没有。从实体框架核心的角度来看,这很好,在这种情况下,AircraftTypeDetail
作为一个纯粹依赖于AircraftType
的对象存在在技术上是非常合适的。
指数
实体框架核心自动为所有外键列分配一个索引。此外,您可以使用 Fluent API 中的方法HasIndex()
来分配任意索引(可能会添加IsUnique()
和ForSqlServerIsClustered()
)。语法比经典的实体框架更简单。然而,与传统的实体框架不同,您不能在实体框架核心中使用数据注释进行索引。
以下是一些例子:
// Index with one column
modelBuilder.Entity<Flight>().HasIndex(x => x.FreeSeats).
// Index with two columns
modelBuilder.Entity<Flight>().HasIndex(f => new {f.Departure, f.Destination});
// Unique Index: Then there could be only one Flight on each Flight route ...
modelBuilder.Entity<Flight>().HasIndex(f => new {f.Departure, f.Destination).IsUnique();
// Unique Index and Clustered Index: there can only be one CI per table (usually PK)
modelBuilder.Entity<Flight>().HasIndex (f => new {f.Departure, f.Destination).IsUnique().ForSqlServerIsClustered();
实体框架核心用前缀IX_
命名数据库中的索引。
Tip
使用HasName()
,您可以影响数据库中索引的名称,就像在modelBuilder.Entity<Flight>().HasIndex(x=>x.FreeSeats).HasName("Index_FreeSeats");
中一样。
在图 6-3 中,有三个外键关系索引,其中一个基于主键。其余两个是手动创建的。
图 6-3
Indexes in SQL Server Management Studio
Fluent API 的语法选项
对于较大的对象模型,实体框架核心上下文类中的OnModelCreating()
方法中的 Fluent API 配置可能会变得非常大。因此,Entity Framework Core 提供了各种不同的选项来构建不同的内容,而不是迄今为止显示的顺序调用。
顺序配置
这些语句的起点是清单 6-2 中所示的实体类Flight
的顺序配置。
modelBuilder.Entity<Flight>().HasKey(f => f.FlightNo);
modelBuilder.Entity<Flight>().Property(b => b.FlightNo).ValueGeneratedNever();
// ----------- Length and null values
modelBuilder.Entity<Flight>().Property(f => f.Memo).HasMaxLength(5000);
modelBuilder.Entity<Flight>().Property(f => f.Seats).IsRequired();
// ----------- Calculated column
modelBuilder.Entity<Flight>().Property(p => p.Utilization)
.HasComputedColumnSql("100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
// ----------- Default values
modelBuilder.Entity<Flight>().Property(x => x.Price).HasDefaultValue(123.45m);
modelBuilder.Entity<Flight>().Property(x => x.Departure).HasDefaultValue("(not set)");
modelBuilder.Entity<Flight>().Property(x => x.Destination).HasDefaultValue("(not set)");
modelBuilder.Entity<Flight>().Property(x => x.Date).HasDefaultValueSql("getdate()");
//// ----------- Indexes
//// Index over one column
modelBuilder.Entity<Flight>().HasIndex(x => x.FreeSeats).HasName("Index_FreeSeats");
//// Index over two columns
modelBuilder.Entity<Flight>().HasIndex(f => new { f.Departure, f.Destination });
Listing 6-2Fluent API Calls for the Entity Class Flight Without Structuring
通过 Lambda 语句构建
这种结构化形式通过在方法Entity()
中输入带有命令序列的 lambda 表达式,消除了modelBuilder.Entity<Flight>()
的不断重复;见清单 6-3 。
modelBuilder.Entity<Flight>(f =>
{
// ----------- PK
f.HasKey(x => x.FlightNo);
f.Property(x => x.FlightNo).ValueGeneratedNever();
//// ----------- Length and null values
f.Property(x => x.Memo).HasMaxLength(5000);
f.Property(x => x.Seats).IsRequired();
// ----------- Calculated column
f.Property(x => x.Utilization)
.HasComputedColumnSql("100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
// ----------- Default values
f.Property(x => x.Price).HasDefaultValue(123.45m);
f.Property(x => x.Departure).HasDefaultValue("(not set)");
f.Property(x => x.Destination).HasDefaultValue("(not set)");
f.Property(x => x.Date).HasDefaultValueSql("getdate()");
// ----------- Indexes
// Index with one column
f.HasIndex(x => x.FreeSeats).HasName("Index_FreeSeats");
// Index with two columns
f.HasIndex(x => new { x.Departure, x.Destination });
});
Listing 6-3Fluent API Calls Structured by Lambda Statement
子程序结构化
在清单 6-4 所示的结构化形式中,实体类的配置存储在一个子程序中。
modelBuilder.Entity<Flight>(ConfigureFlight);
private void ConfigureFlight(EntityTypeBuilder<Flight> f)
{
// ----------- PK
f.HasKey(x => x.FlightNo);
f.Property(x => x.FlightNo).ValueGeneratedNever();
//// ----------- Length and null values
f.Property(x => x.Memo).HasMaxLength(5000);
f.Property(x => x.Seats).IsRequired();
// ----------- Calculated column
f.Property(x => x.Utilization)
.HasComputedColumnSql("100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
// ----------- Default values
f.Property(x => x.Price).HasDefaultValue(123.45m);
f.Property(x => x.Departure).HasDefaultValue("(not set)");
f.Property(x => x.Destination).HasDefaultValue("(not set)");
f.Property(x => x.Date).HasDefaultValueSql("getdate()");
// ----------- Indexes
// Index with one column
f.HasIndex(x => x.FreeSeats).HasName("Index_FreeSeats");
// Index with two columns
f.HasIndex(x => new { x.Departure, x.Destination });
}
Listing 6-4Fluent API Calls Structured by Subroutine
通过配置类构建
在 Entity Framework Core 2.0 中,微软引入了另一个结构化选项。继经典实体框架中存在的EntityTypeConfiguration<T>
继承类中的配置外包之后,实体框架核心现在提供了IEntityTypeConfiguration <EntityType>
接口,使用该接口可以为实体类型实现单独的配置类;见清单 6-5 。
using BO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DA
{
/// <summary>
/// Configuration Class for Entity Class Flight
/// EFCore >= 2.0
/// </summary>
class FlightETC : IEntityTypeConfiguration<Flight>
{
public void Configure(EntityTypeBuilder<Flight> f)
{
// ----------- PK
f.HasKey(x => x.FlightNo);
f.Property(x => x.FlightNo).ValueGeneratedNever();
//// ----------- Length and null values
f.Property(x => x.Memo).HasMaxLength(5000);
f.Property(x => x.Seats).IsRequired();
// ----------- Calculated column
f.Property(x => x.Utilization)
.HasComputedColumnSql("100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
// ----------- Default values
f.Property(x => x.Price).HasDefaultValue(123.45m);
f.Property(x => x.Departure).HasDefaultValue("(not set)");
f.Property(x => x.Destination).HasDefaultValue("(not set)");
f.Property(x => x.Date).HasDefaultValueSql("getdate()");
// ----------- Indexes
// Index with one column
f.HasIndex(x => x.FreeSeats).HasName("Index_FreeSeats");
// Index with two columns
f.HasIndex(x => new { x.Departure, x.Destination });
}
}
}
Listing 6-5Fluent API Calls Structured by IEntityTypeConfiguration
您可以通过调用OnModelCreating()
中的modelBuilder.ApplyConfiguration <EntityClass>(ConfigurationObject)
来使用这个配置类,如下所示:
modelBuilder.ApplyConfiguration<Flight>(new FlightETC());
使用 Fluent API 进行批量配置
Fluent API 中的另一个选项是不配置每个单独的实体类,而是一次配置几个。被传递的ModelBuilder
对象的子对象Model
通过GetEntityTypes()
用接口IMutableEntityType
以对象的形式提供了所有实体类的列表。此接口提供对实体类的所有配置选项的访问。清单 6-6 中的示例显示如下:
- 它避免了上下文类中所有表名都被命名为属性名
DbSet<EntityClass>
的惯例。使用entity.Relational().TableName = entity.DisplayName()
,所有的表都被命名为实体类。例外只是那些有[Table ]
注释的类,所以你有机会设置与规则的个别偏差。 - 确保以字母
NO
结尾的属性自动成为主键,并且这些主键没有自动递增的值。
protected override void OnModelCreating (ModelBuilder builder)
{
...
#region Bulk configuration via model class for all table names
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
// All table names = class names (~ EF 6.x),
// except the classes that have a [Table] annotation
var annotation = entity.ClrType.GetCustomAttribute<TableAttribute>();
if (annotation == null)
{
entity.Relational().TableName = entity.DisplayName();
}
}
...
#region Bulk configuration via model class for primary key
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
// properties ending in the letters "NO" automatically become the primary key and there are no auto increment values for these primary keys.
var propNr = entity.GetProperties().FirstOrDefault(x => x.Name.EndsWith("No"));
if (propNr != null)
{
entity.SetPrimaryKey(propNr);
propNr.ValueGenerated = ValueGenerated.Never;
}
}
}
Listing 6-6Bulk Configuration in the Fluent API
七、数据库模式迁移
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
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
十、插入、删除和修改记录
在许多地方,与传统的实体框架相比,在实体框架核心中插入、删除和修改记录的 API 和方法保持不变。但是在细节上有一些变化,特别是当将多个变化合并到数据库管理系统的一个批处理往返中时。
您可以随时写入从数据库加载的实体对象。您不必在写操作之前“宣布”它们,也不必在写操作之后“注册它们”。实体框架核心的上下文类(更准确地说,是内置的变更跟踪器)跟踪标准系统中对象的所有变更(称为变更跟踪)。但是,如果对象是在非跟踪模式(例如使用AsNoTracking()
)下加载的,则不会发生更改跟踪,这是专门设置的,或者上下文实例被破坏。
使用 SaveChanges()保存
清单 10-1 展示了如何用SingleOrDefault()
加载一个Flight
对象。在这个Flight
对象中,自由席位的数量减少了两个位置。此外,一些文本被写到Flight
对象的memo
属性中。
SaveChanges()
方法用于存储数据库中的更改。它在基类DbContext
中实现,并从那里继承到您在逆向工程中生成的上下文类,或者它在正向工程中创建自己。
SaveChanges()
方法保存自加载以来的所有更改(新记录、已更改的记录和已删除的记录),或者保存当前上下文实例中加载的所有对象上的最后一个SaveChanges()
方法。SaveChanges()
向数据库发送一个INSERT
、UPDATE
或DELETE
命令来保存每个更改。
Note
即使在实体框架核心中,不幸的是,当存在多个变更时,也不可能只保存单个变更。
当然,SaveChanges()
只保存被改变的对象和被改变对象的被改变的属性。图 10-1 中显示的 SQL 输出证明了这一点。在UPDATE
命令的SET
部分,只有FreeSeats
和Memo
出现。您还可以看到,UPDATE
命令返回已更改记录的数量。调用者从SaveChanges()
接收这个数字作为返回值。
UPDATE
命令只包含WHERE
条件下的FlightNo
值。换句话说,这里没有检查对象是否被另一个用户或后台进程更改。实体框架核心的标准是“最后一个胜出”的原则但是,您可以更改这种默认行为(参见第十七章)。只有当数据库中要更改的数据记录被删除时,调用者才会得到类型为DbConcurrencyException
的运行时错误。然后,UPDATE
命令从数据库管理系统返回零个记录受变更影响,实体框架核心将此视为变更冲突的指示。
清单 10-1 打印了三次关于Flight
对象的信息(更改前、更改后和保存后)。除了FlightNo
(主键)和Flight
路线(出发地和目的地),还打印FreeSeats
的编号和对象的当前状态。状态不能由实体对象本身决定,只能由带有ctx.Entry(obj).State
的上下文类的Entry()
方法决定。
public static void ChangeFlightOneProperty()
{
CUI.MainHeadline(nameof(ChangeFlightOneProperty));
int FlightNr = 101;
using (WWWingsContext ctx = new WWWingsContext())
{
// Load flight
var f = ctx.FlightSet.Find(FlightNr);
Console.WriteLine($"Before changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Change object in RAM
f.FreeSeats -= 2;
Console.WriteLine($"After changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Persist changes
try
{
var count = ctx.SaveChanges();
if (count == 0)
{
Console.WriteLine("Problem: No changes saved!");
}
else
{
Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
}
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 10-1One changed Property of Flights is Saved
图 10-1 从实体框架核心的角度展示了对象的状态是如何变化的。装完后就是Unchanged
。将 change 设置为Modified
后,实体框架核心就知道对象发生了变化。用SaveChanges()
救了之后,又是Unchanged
。换句话说,RAM 中的状态再次对应于数据库中的状态。
当执行SaveChanges()
方法时,很可能出现错误(例如,dbConcurrencyException
)。因此,在清单 10-1 中,有明确的try-catch
到SaveChanges()
。SaveChanges()
上另一个典型的运行时错误是当。NET Framework 允许从数据库的角度写入未经授权的值。例如,如果这个列在数据库中有长度限制,那么使用Memo
属性就会发生这种情况。既然琴弦在。NET 的长度基本上是无限的,从数据库的角度来看,可能会给属性分配一个太长的字符串。
Note
与经典的实体框架不同,实体框架核心在用SaveChanges()
保存之前不做任何验证。换句话说,无效值首先被数据库管理系统注意到。在这种情况下,您会得到以下运行时错误:“Microsoft。EntityFrameworkCore . DbUpdateException:更新条目时出错。有关详细信息,请参见内部异常。然后,内部异常对象提供了错误的实际来源:“System。DataSqlClient.SqlException:字符串或二进制数据将被截断。
图 10-1
Output of Listing 10-1
下面是清单 10-1 中实体框架核心在SaveChanges()
发出的 SQL 命令:
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 smallint',@p1=101,@p0=114'
跟踪子对象的变更
变更跟踪在 Entity Framework Core(与其前身一样)中对变更的子对象起作用。清单 10-2 加载Flight
对象及其Pilot
对象。对Flight
对象以及连接的Pilot
对象进行更改(增加Pilot
的Flight
时间)。Pilot
对象的状态类似于Flight
对象,从Unchanged
变为Modified
,并在SaveChanges()
执行后再次变为Unchanged
。
public static void ChangeFlightAndPilot()
{
CUI.MainHeadline(nameof(ChangeFlightAndPilot));
int flightNo = 101;
using (WWWingsContext ctx = new WWWingsContext())
{
var f = ctx.FlightSet.Include(x => x.Pilot).SingleOrDefault(x => x.FlightNo == flightNo);
Console.WriteLine($"After loading: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats!\nState of the flight object: " + ctx.Entry(f).State + " / State of the Pilot object: " + ctx.Entry(f.Pilot).State);
f.FreeSeats -= 2;
f.Pilot.FlightHours = (f.Pilot.FlightHours ?? 0) + 10;
f.Memo = $"Changed by User {System.Environment.UserName} on {DateTime.Now}.";
Console.WriteLine($"After changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats!\nState of the flight object: " + ctx.Entry(f).State + " / State of the Pilot object: " + ctx.Entry(f.Pilot).State);
try
{
var count = ctx.SaveChanges();
if (count == 0) Console.WriteLine("Problem: No changes saved!");
else Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats!\nState of the flight object: " + ctx.Entry(f).State + " / State of the Pilot object: " + ctx.Entry(f.Pilot).State);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 10-2Changes in Subobjects
以下是实体框架核心发送给SaveChanges()
的 SQL 命令;它显示两个UPDATE
命令被发送到数据库管理系统。图 10-2 显示了输出。
图 10-2
Output of the previous code
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Employee] SET [FlightHours] = @p0
WHERE [PersonID] = @p1;
SELECT @@ROWCOUNT;
UPDATE [Flight] SET [FreeSeats] = @p2, [Memo] = @p3
WHERE [FlightNo] = @p4;
SELECT @@ ROWCOUNT;
',N'@p1 int,@p0 int,@p4 int,@p2 smallint,@p3 nvarchar(4000)',@p1=57,@p0=40,@p4=101,@p2=104,@p3=N'Changed by User HS on 23/12/2017 00:53:12.'
组合命令(批处理)
与经典的实体框架相反,实体框架核心并不在其自身的往返行程中将每个INSERT
、UPDATE
或DELETE
命令发送到数据库管理系统;相反,它将命令组合成更大的往返行程。这种功能称为批处理。
实体框架核心决定往返的命令摘要的大小。在对Flight
数据集的大规模插入的测试中,300 个数据集用于两次往返;1000 次用于六次往返旅行;2000 被用于 11;5000 次用于数据库管理系统的 27 次往返。
除了Add()
方法之外,在上下文类和DbSet<EntityClass>
类上都有一个AddRange()
方法,您可以向其传递要附加的对象列表。在经典的实体框架中,AddRange()
比Add()
要快得多,因为它消除了重复审查实体框架变更跟踪程序的需要。实体框架核心不再有在一个循环中调用Add()
1000 次或者用一组 1000 个对象作为参数调用AddRange()
一次的性能差异。图 10-3 中一个清晰可见的性能优势是由批处理产生的。但是如果你总是在Add()
之后直接调用SaveChanges()
,就不可能进行批处理(见图 10-3 中的第三条)。
图 10-3
Power measurement during mass insertion of 1,000 records
处理 foreach 循环注意事项
使用 Entity Framework,在使用转换操作符(如ToList()
)进行迭代之前,没有必要显式具体化查询。一个带有IQueryable
接口的对象上的foreach
循环足以触发数据库查询。然而,在这种情况下,当循环运行时,数据库连接保持打开,记录由IQueryable
接口的迭代器单独获取。这导致在数据获取foreach
循环中调用SaveChanges()
导致运行时错误,如清单 10-3 和图 10-4 所示。
这里列出了三种解决方案:
- 最好的解决方案是在开始循环之前用
ToList()
完全具体化查询,并将SaveChanges()
放在循环之后。这导致在一次或几次往返中传输所有变化。但是所有的变化都有一个交易! - 如果请求多个事务中的改变,那么在循环之前至少应该执行
ToList()
。 - 或者,可以用
SaveChangesAsync()
代替SaveChanges()
;更多信息见第十三章。
Tip
使用带有ToList()
的显式物化。
图 10-4
Error from running Listing 10-3
public static void Demo_ForeachProblem()
{
CUI.Headline(nameof(Demo_ForeachProblem));
WWWingsContext ctx = new WWWingsContext();
// Define query
var query = (from f in ctx.FlightSet.Include(p => p.BookingSet).ThenInclude(b => b.Passenger) where f.Departure == "Rome" && f.FreeSeats > 0 select f).Take(1);
// Query is performed implicitly by foreach
foreach (var Flight in query)
{
// Print results
CUI.Print("Flight: " + Flight.FlightNo + " from " + Flight.Departure + " to " + Flight.Destination + " has " + Flight.FreeSeats + " free seats");
foreach (var p in Flight.BookingSet)
{
CUI.Print(" Passenger " + p.Passenger.GivenName + " " + p.Passenger.Surname);
}
// Save change to every flight object within the loop
CUI.Print(" Start saving");
Flight.FreeSeats--;
ctx.SaveChangesAsync(); // SaveChanges() will produce ERROR!!!
CUI.Print(" End saving");
}
}
Listing 10-3SaveChanges() Does Not Work Within a foreach Loop Unless You Have Previously Materialized the Records
添加新对象
要添加一条新记录(在 SQL 中,使用INSERT
),您可以使用实体框架核心执行以下步骤:
- 用
new
操作符实例化对象(像往常一样在。网)。与经典实体框架中一样,工厂方法在实体框架核心中并不存在。 - 从数据库模式的角度来看,填充对象,尤其是所有强制属性。
- 通过 context 类中的
Add()
方法或 context 类中适当的DbSet<EntityClass>
将对象附加到上下文中。 - 调用
SaveChanges()
。
清单 10-4 展示了如何创建一个Flight
对象。强制要求是航班号(这里的主键不是自动增加的值,因此必须手动设置)、航线、出发、目的地和日期,以及与Pilot
对象的关系。副驾驶是可选的。即使航空公司是强制字段,程序代码也可以在没有显式分配枚举值的情况下工作,因为实体框架核心将在这里使用默认值 0,这是数据库的有效值。
作为预先加载Pilot
对象然后将其分配给Flight
对象的替代方法,您可以通过使用Flight
对象中的外键属性PilotId
并在那里直接分配Pilot
对象的主键:f.PilotId = 234
来更有效地实现该任务。在这里,您将看到显式外键属性的优点,它可以节省数据库的往返行程。
public static void AddFlight()
{
CUI.MainHeadline(nameof(AddFlight));
using (WWWingsContext ctx = new WWWingsContext())
{
// Create flight in RAM
var f = new Flight();
f.FlightNo = 123456;
f.Departure = "Essen";
f.Destination = "Sydney";
f.AirlineCode = "WWW";
f.PilotId = ctx.PilotSet.FirstOrDefault().PersonID;
f.Seats = 100;
f.FreeSeats = 100;
Console.WriteLine($"Before adding: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the Flight object: " + ctx.Entry(f).State);
// Add flight to context
ctx.FlightSet.Add(f);
// or: ctx.Add(f);
Console.WriteLine($"After adding: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the Flight object: " + ctx.Entry(f).State);
try
{
var count = ctx.SaveChanges();
if (count == 0) Console.WriteLine("Problem: No changes saved!");
else Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the Flight object: " + ctx.Entry(f).State);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 10-4Creating a New Flight
对象状态的顺序如下(见图 10-5 ): Detached
(在执行Add()
之前,实体框架核心上下文不知道Flight
的新实例,因此认为它是一个瞬态对象),Added
(在add()
之后),然后在保存Unchanged
之后。顺便说一句,实体框架核心并不认为Add()
的多次调用是错误的,但是Add()
不需要被多次调用。
Note
您可以添加一个主键值尚不存在于当前上下文实例中的对象。如果你想删除一个对象,然后在相同的主键值下创建一个新的对象,你必须在Remove()
之后、Add()
之前执行SaveChanges()
;否则,实体框架核心会报错,并显示以下错误消息:“System。InvalidOperationException:无法跟踪实体类型“Flight”的实例,因为已经在跟踪{“Flight no”}的另一个具有相同键值的实例。
图 10-5
Output from Listing 10-4 (creating a Flight object)
创建相关对象
Add()
方法不仅考虑作为参数传递的对象,还考虑与该对象相关联的对象。如果在状态Detached
中作为参数传递的对象下有对象,它们被自动添加到上下文中,然后处于状态Added
。
1:N 情况下的关系操作是在第 1 方通过用特定于列表的方法添加和删除列表来完成的,主要是用方法Add()
和Remove()
。对于双向关系,可以在第 1 端或第 N 端进行更改。在Flight
和Pilot
之间的双向关系的具体情况下,有三种等价的建立关系的方式,这里列出:
- 利用 1 侧的导航属性,所以
Flight
:flight.Pilot = Pilot;
中的Pilot
- 使用
Flight
:flight.PersonID = 123;
中的外键属性PersonID
- 使用 N 端的导航属性,换句话说,
Pilot
页面上的:】页
清单 10-5 展示了如何用一个新的Pilot
对象、一个新的AircraftType
对象和一个新的AircraftTypeDetail
对象创建一个新的Flight
。对Flight
对象执行Add()
就足够了。然后,实体框架核心向数据库发送SaveChanges()
五个INSERT
命令,每个命令对应一个表:AircraftType
、AircraftTypeDetail
、Employees
(Employee
和Pilot
类实例的公共表)、Persondetail
和Flight
。
public static void Demo_CreateRelatedObjects()
{
CUI.MainHeadline(nameof(Demo_CreateRelatedObjects));
using (var ctx = new WWWingsContext())
{
ctx.Database.ExecuteSqlCommand("Delete from Booking where FlightNo = 456789");
ctx.Database.ExecuteSqlCommand("Delete from Flight where FlightNo = 456789");
var p = new Pilot();
p.GivenName = "Holger";
p.Surname = "Schwichtenberg";
p.HireDate = DateTime.Now;
p.LicenseDate = DateTime.Now;
var pd = new Persondetail();
pd.City = "Essen";
pd.Country = "DE";
p.Detail = pd;
var act = new AircraftType();
act.TypeID = (byte)(ctx.AircraftTypeSet.Max(x=>x.TypeID)+1);
act.Manufacturer = "Airbus";
act.Name = "A380-800";
ctx.AircraftTypeSet.Add(act);
ctx.SaveChanges();
var actd = new AircraftTypeDetail();
actd.TurbineCount = 4;
actd.Length = 72.30f;
actd.Tare = 275;
act.Detail = actd;
var f = new Flight();
f.FlightNo = 456789;
f.Pilot = p;
f.Copilot = null;
f.Seats = 850;
f.FreeSeats = 850;
f.AircraftType = act;
// One Add() is enough for all related objects!
ctx.FlightSet.Add(f);
ctx.SaveChanges();
CUI.Print("Total number of flights: " + ctx.FlightSet.Count());
CUI.Print("Total number of pilots: " + ctx.PilotSet.Count());
}
}
Listing 10-5Creation of a New Pilot’s Flight with Persondetail, New AircraftType, and AircraftTypeDetail
图 10-6 显示了这五个INSERT
命令的输出。
图 10-6
Batch updating makes only three round-trips for five INSERT commands
更改链接对象
实体框架核心还检测实体对象之间的关系变化,并自动保存在SaveChanges()
中。就像最初用Pilot
创建一个Flight
对象一样,在理想情况下有三个改变关系的选项(带有外键属性的双向关系):
- 使用
Flight
:Flight.Pilot = Pilot;
中的导航属性Pilot
- 使用
Flight
:Flight.PersonID = 123;
中的外键属性PilotId
- 使用
Pilot
页面上的导航属性FlightAsPilotSet
:Pilot.FlightAsPilotSet.Add (Flight);
在所有这三种情况下,实体框架核心都向数据库发送SaveChanges()
。通过执行SaveChanges()
,实体框架核心正确地不对Pilot
表中的数据库进行任何更改,而是对Flight
表中的数据库进行任何更改,因为Flight
表具有建立Pilot
和Flight
之间关系的外键。
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Flight] SET [PilotId] = @p0
WHERE [FlightNo] = @p1;
SELECT @@ ROWCOUNT;
', N' @p1 int, @p0 int ', @p1 = 101, @p0 = 123
Tip
要删除关系,您可以简单地分配零或什么都不分配。
清单 10-6 显示了一个新Pilot
到一个Flight
的赋值。然而,这种分配不是通过Flight101.Pilot = newPilot
(1 侧)进行的,而是通过newPilot.FlightAsPilotSet.Add(flight101)
在Pilot
侧(N 侧)进行的。这个清单的输出是令人兴奋的;见图 10-7 。你可以看到,一开始,一个飞行员有 31 次飞行,另一个有 10 次飞行。分配后,新的Pilot
有 11 个班次,旧的Pilot
还是 31 个班次,不对。另外,flight101.Pilot
还是指旧的Pilot
,这也不对。
然而,在运行SaveChanges()
之后,对象关系已经被纠正。现在旧的Pilot
只有 30 个航班。另外,flight101.Pilot
指的是新的Pilot
。实体框架核心的这一特性被称为关系修复。作为关系修正操作的一部分,实体框架核心检查当前在 RAM 中的对象之间的所有关系,并且如果在另一侧发生了变化,也在另一侧改变它们。使用SaveChanges()
保存时,实体框架核心运行关系修复操作。
通过执行方法ctx.ChangeTracker.DetectChanges()
,您可以在任何时候强制执行关系修复操作。如果许多对象已经被加载到一个上下文实例中,DetectChanges()
可能会花费许多毫秒。因此,在实体框架核心中,微软不会在很多地方自动调用DetectChanges()
,而是由您来决定何时需要对象关系的一致状态并使用DetectChanges()
。图 10-7 显示了输出。
图 10-7
Output of Listing 10-6 (the relationship fixup works)
public static void Demo_RelationhipFixup1N()
{
CUI.MainHeadline(nameof(Demo_RelationhipFixup1N));
using (var ctx = new WWWingsContext())
{
// Load a flight
var flight101 = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {flight101.FlightNo} from {flight101.Departure} to {flight101.Destination} has {flight101.FreeSeats} free seats!");
// Load the pilot for this flight with the list of his flights
var oldPilot = ctx.PilotSet.Include(x => x.FlightAsPilotSet).SingleOrDefault(x => x.PersonID == flight101.PilotId);
Console.WriteLine("Pilot: " + oldPilot.PersonID + ": " + oldPilot.GivenName + " " + oldPilot.Surname + " has " + oldPilot.FlightAsPilotSet.Count + " flights as pilot!");
// Next pilot in the list load with the list of his flights
var newPilot = ctx.PilotSet.Include(x => x.FlightAsPilotSet).SingleOrDefault(x => x.PersonID == flight101.PilotId + 1);
Console.WriteLine("Planned Pilot: " + newPilot.PersonID + ": " + newPilot.GivenName + " " + newPilot.Surname + " has " + newPilot.FlightAsPilotSet.Count + " flights as pilot!");
// Assign to Flight
CUI.Print("Assignment of the flight to the planned pilot...", ConsoleColor.Cyan);
newPilot.FlightAsPilotSet.Add(flight101);
// optional:force Relationship Fixup
// ctx.ChangeTracker.DetectChanges();
CUI.Print("Output before saving: ", ConsoleColor.Cyan);
Console.WriteLine("Old pilot: " + oldPilot.PersonID + ": " + oldPilot.GivenName + " " + oldPilot.Surname + " has " + oldPilot.FlightAsPilotSet.Count + " flights as a pilot!");
Console.WriteLine("New pilot: " + newPilot.PersonID + ": " + newPilot.GivenName + " " + newPilot.Surname + " has " + newPilot.FlightAsPilotSet.Count + " flights as a pilot!");
var pilotAktuell = flight101.Pilot; // Current Pilot in the Flight object
Console.WriteLine("Pilot for flight " + flight101.FlightNo + " is currently: " + pilotAktuell.PersonID + ": " + pilotAktuell.GivenName + " " + pilotAktuell.Surname);
// SaveChanges()()
CUI.Print("Saving... ", ConsoleColor.Cyan);
var count = ctx.SaveChanges();
CUI.MainHeadline("Number of saved changes: " + count);
CUI.Print("Output after saving: ", ConsoleColor.Cyan);
Console.WriteLine("Old Pilot: " + oldPilot.PersonID + ": " + oldPilot.GivenName + " " + pilotAlt.Surname + " has " + pilotAlt.FlightAsPilotSet.Count + " flights as pilot!");
Console.WriteLine("New Pilot: " + newPilot.PersonID + ": " + newPilot.GivenName + " " + newPilot.Surname + " has " + newPilot.FlightAsPilotSet.Count + " flights as pilot!");
pilotAktuell = flight101.Pilot; // Current pilot from the perspective of the Flight object
Console.WriteLine("Pilot for Flight " + flight101.FlightNo + " is now: " + pilotAktuell.PersonID + ": " + pilotAktuell.GivenName + " " + pilotAktuell.Surname);
}
}
Listing 10-6Making a 1:N Relationship Across the First Page
处理矛盾的关系
如果像前面解释的那样,有多达三种方法来建立对象之间的关系,那么当多个选项与矛盾的数据并行使用时会发生什么?
列表 10-7 ,结合图 10-8 和图 10-9 的输出,显示优先级如下:
- 最高优先级是设置在 1 侧的对象的值,所以在关系
Pilot<->Flight
(1:N)的情况下,来自Pilot.FlightAsPilotSet
的值是第一个。 - 第二高的优先级是来自 N 侧的单个对象的值。换句话说,在
Pilot<->Flight
(1:N)的情况下,是来自Flight.Pilot
的值。 - 只有这样,N 端才会考虑外键属性。换句话说,在
Pilot: Flight
(1:N)的情况下,是来自Flight.PersonID
的值。
图 10-9
Output of Listing 10-7 (part 2)
图 10-8
Output of Listing 10-7 (part 1)
using System;
using System.Linq;
using DA;
using EFC_Console;
using ITVisions;
using Microsoft.EntityFrameworkCore;
namespace EFC_Console
{
class ContradictoryRelationships
{
/// <summary>
/// Four test scenarios for the question of which value has priority, if the relationship is set contradictory
/// </summary>
[EFCBook()]
public static void Demo_ContradictoryRelationships()
{
CUI.MainHeadline(nameof(Demo_ContradictoryRelationships));
Attempt1();
Attempt2();
Attempt3();
Attempt4();
}
public static int pilotID = new WWWingsContext().PilotSet.Min(x => x.PersonID);
public static int GetPilotIdEinesFreienPilots()
{
// here we assume that the next one in the list has time for this flight :-)
pilotID++; return pilotID;
}
private static void Attempt1()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline("Attempt 1: first assignment by navigation property, then by foreign key property");
CUI.PrintStep("Load a flight...");
var flight101 = ctx.FlightSet.Include(f => f.Pilot).SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {flight101.FlightNo} from {flight101.Departure} to {flight101.Destination} has {flight101.FreeSeats} free seats!");
CUI.Print("Pilot object: " + flight101.Pilot.PersonID + " PilotId: " + flight101.PilotId);
CUI.PrintStep("Load another pilot...");
var newPilot2 = ctx.PilotSet.Find(GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot2.PersonID} via navigation property...");
flight101.Pilot = newPilot2;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Reassign a new pilot via foreign key property...");
var neuePilotID = GetPilotIdEinesFreienPilots();
CUI.PrintStep($"Assign a new pilot #{neuePilotID} via foreign key property...");
flight101.PilotId = neuePilotID;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("SaveChanges()");
var anz2 = ctx.SaveChanges();
CUI.PrintSuccess("Number of saved changes: " + anz2);
CUI.PrintStep("Control output after saving: ");
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
}
}
private static void Attempt2()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline("Attempt 2: First assignment by foreign key property, then navigation property");
CUI.PrintStep("Load a flight...");
var flight101 = ctx.FlightSet.Include(f => f.Pilot).SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {flight101.FlightNo} from {flight101.Departure} to {flight101.Destination} has {flight101.FreeSeats} free seats!");
CUI.Print("Pilot object: " + flight101.Pilot.PersonID + " PilotId: " + flight101.PilotId);
var neuePilotID2 = GetPilotIdEinesFreienPilots();
CUI.PrintStep($"Assign a new pilot #{neuePilotID2} via foreign key property...");
flight101.PilotId = neuePilotID2;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Load another pilot...");
var newPilot1 = ctx.PilotSet.Find(GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot1.PersonID} via navigation property...");
flight101.Pilot = newPilot1;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("SaveChanges()");
var anz2 = ctx.SaveChanges();
CUI.PrintSuccess("Number of saved changes: " + anz2);
CUI.PrintStep("Control output after saving: ");
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
}
}
private static void Attempt3()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline("Attempt 3: Assignment using FK, then Navigation Property at Flight, then Navigation Property at Pilot");
CUI.PrintStep("Load a flight...");
var flight101 = ctx.FlightSet.Include(f => f.Pilot).SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight No {flight101.FlightNo} from {flight101.Departure} to {flight101.Destination} has {flight101.FreeSeats} free seats!");
CUI.Print("Pilot object: " + flight101.Pilot.PersonID + " PilotId: " + flight101.PilotId);
var neuePilotID3 = GetPilotIdEinesFreienPilots();
CUI.PrintStep($"Assign a new pilot #{neuePilotID3} via foreign key property...");
flight101.PilotId = neuePilotID3;
CUI.Print("flight101.PilotId=" + flight101.PilotId);
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Load another pilot...");
var newPilot3a = ctx.PilotSet.Find(GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot3a.PersonID} via navigation property in Flight object...");
flight101.Pilot = newPilot3a;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Load another Pilot...");
var newPilot3b = ctx.PilotSet.Include(p => p.FlightAsPilotSet).SingleOrDefault(p => p.PersonID == GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot3b.PersonID} via navigation property in Pilot object...");
newPilot3b.FlightAsPilotSet.Add(flight101);
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("SaveChanges()");
var anz3 = ctx.SaveChanges();
CUI.PrintSuccess("Number of saved changes: " + anz3);
CUI.PrintStep("Control output after saving: ");
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
}
}
private static void Attempt4()
{
using (var ctx = new WWWingsContext())
{
CUI.MainHeadline("Attempt 4: First assignment by FK, then Navigation Property at Pilot, then Navigation Property at Flight");
CUI.PrintStep("Load a flight...");
var flight101 = ctx.FlightSet.Include(f => f.Pilot).SingleOrDefault(x => x.FlightNo == 101);
Console.WriteLine($"Flight Nr {flight101.FlightNo} from {flight101.Departure} to {flight101.Destination} has {flight101.FreeSeats} free seats!");
CUI.Print("Pilot object: " + flight101.Pilot.PersonID + " PilotId: " + flight101.PilotId);
var neuePilotID4 = GetPilotIdEinesFreienPilots();
CUI.PrintStep($"Assign a new pilot #{neuePilotID4} via foreign key property...");
flight101.PilotId = neuePilotID4;
CUI.Print("flight101.PilotId=" + flight101.PilotId);
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Load another pilot...");
var newPilot4b = Queryable.SingleOrDefault(ctx.PilotSet.Include(p => p.FlightAsPilotSet), p => p.PersonID == GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot4b.PersonID} via navigation property...");
newPilot4b.FlightAsPilotSet.Add(flight101);
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("Load another Pilot...");
var newPilot4a = ctx.PilotSet.Find(GetPilotIdEinesFreienPilots()); // next Pilot
CUI.PrintStep($"Assign a new pilot #{newPilot4a.PersonID} via navigation property in Flight object...");
flight101.Pilot = newPilot4a;
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
CUI.PrintStep("SaveChanges()");
var anz4 = ctx.SaveChanges();
CUI.PrintSuccess("Number of saved changes: " + anz4);
CUI.PrintStep("Control output after saving: ");
CUI.Print($"PilotId: {flight101.PilotId} Pilot object: {flight101.Pilot?.PersonID}");
}
}
}
}
Listing 10-7Four Test Scenarios for the Question of Which Value Has Priority When the Relationship Is Contradictory
删除对象
本节介绍删除对象和数据库中相应行的不同方法。
使用 Remove()删除对象
要删除一个对象,你必须调用Remove()
方法,它和Add()
一样,要么直接存在于上下文类中(从DbContext
继承而来),要么存在于上下文类的DbSet<EntityClass>
属性中(参见清单 10-8 )。调用Remove()
导致加载的Flight
对象从Unchanged
状态变为Delete
状态(见图 10-10 )。但是,它尚未从数据库中删除。只有通过调用SaveChanges()
方法,才会向数据库管理系统发送一个DELETE
命令。
图 10-10
Output of Listing 10-8
public static void RemoveFlight()
{
CUI.MainHeadline(nameof(RemoveFlight));
using (WWWingsContext ctx = new WWWingsContext())
{
var f = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == 123456);
if (f == null) return;
Console.WriteLine($"After loading: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Remove flight
ctx.FlightSet.Remove(f);
// or: ctx.Remove(f);
Console.WriteLine($"After deleting: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
try
{
var count = ctx.SaveChanges();
if (count == 0) Console.WriteLine("Problem: No changes saved!");
else Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 10-8Deleting a Flight Record
下面是清单 10-8 中实体框架核心发出的 SQL 命令:
SELECT TOP(2) [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].[Utilization]
FROM [Flight] AS [x]
WHERE [x].[FlightNo] = 123456
exec sp_executesql N'SET NOCOUNT ON;
DELETE FROM [Flight]
WHERE [FlightNo] = @p0;
SELECT @@ROWCOUNT;
',N'@p0 int',@p0=123456
删除带有虚拟对象的对象
在前面的代码中,完全加载Flight
对象效率很低;您只发送删除命令。清单 10-9 显示了一个解决方案,它通过在 RAM 中创建一个Flight
对象来避免这种到数据库管理系统的往返,其中只有主键被设置为要删除的对象。然后用Attach()
将这个虚拟对象附加到上下文中。这使得对象的状态从Detached
变为Unchanged
。最后,你执行Remove()
和SaveChanges()
。这个技巧是可行的,因为实体框架只需要知道删除的主键。
请注意以下关于此技巧的内容:
- 这里调用的是方法
Attach()
,不是Add()
;否则,实体框架核心会将虚拟对象视为新对象。 - 只有在实体框架核心中没有配置冲突检查时,这个技巧才有效。但是,如果模型设置为在保存时比较其他列的值,则必须用虚拟对象中的当前值填充这些值。否则,无法删除对象,并出现
DbConcurrenyException
。
public static void RemoveFlightWithKey()
{
Console.WriteLine(nameof(RemoveFlightWithKey));
using (WWWingsContext ctx = new WWWingsContext())
{
// Create a dummy object
var f = new Flight();
f.FlightNo = 123456;
Console.WriteLine($"After creation: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Append dummy object to context
ctx.Attach(f);
Console.WriteLine($"After attach: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Delete flight
ctx.FlightSet.Remove(f);
// or: ctx.Remove(f);
Console.WriteLine($"After remove: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
try
{
var count = ctx.SaveChanges();
if (count == 0) Console.WriteLine("Problem: No changes saved!");
else Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 10-9Deleting a Flight Record More Efficiently with a Dummy Object
批量删除
Remove()
方法不适合Delete from Flight where FlightNo> 10000
中定义的批量删除,因为实体框架核心将在每种情况下为每个对象生成一个DELETE
命令。实体框架核心没有认识到许多DELETE
命令可以合并成一个命令。在这种情况下,您应该总是依赖经典技术(SQL 或存储过程),因为在这里使用Remove()
会慢很多倍。另一个选项是扩展 EFPlus(参见第二十章)。
执行数据库事务
请注意以下关于数据库事务的要点:
- 当您运行
SaveChanges()
时,Entity Framework Core 总是自动进行一个事务,这意味着在上下文中所做的所有更改都被持久化,或者都不被持久化。 - 如果您需要一个跨越对
SaveChanges()
方法的多次调用的事务,您必须使用ctx.Database.BeginTransaction()
、Commit()
和Rollback()
来完成。 System.Transactions.Transactions.TransactionScope
在实体框架核心中尚不支持。实体框架核心 2.1 版将支持;参见附录 C 。
Tip
最好的交易是你避免的交易。事务总是对应用的性能、可伸缩性和健壮性产生负面影响。
例 1
以下示例显示了对一个Flight
对象进行两次更改的事务,这两次更改分别由SaveChanges()
独立保存:
public static void ExplicitTransactionTwoSaveChanges()
{
Console.WriteLine(nameof(ExplicitTransactionTwoSaveChanges));
using (var ctx = new WWWingsContext())
{
// Start transaction. Default is System.Data.IsolationLevel.ReadCommitted
using (var t = ctx.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
{
// Print isolation level
RelationalTransaction rt = t as RelationalTransaction;
DbTransaction dbt = rt.GetDbTransaction();
Console.WriteLine("Transaction with Level: " + dbt.IsolationLevel);
// Read data
int FlightNr = ctx.FlightSet.OrderBy(x => x.FlightNo).FirstOrDefault().FlightNo;
var f = ctx.FlightSet.Where(x => x.FlightNo == FlightNr).SingleOrDefault();
Console.WriteLine("Before: " + f.ToString());
// Change data and save
f.FreeSeats--;
var count1 = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count1);
// Change data again and save
f.Memo = "last changed at " + DateTime.Now.ToString();
var count2 = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count2);
Console.WriteLine("Commit or Rollback? 1 = Commit, other = Rollback");
var input = Console.ReadKey().Key;
if (input == ConsoleKey.D1)
{ t.Commit(); Console.WriteLine("Commit done!"); }
else
{ t.Rollback(); Console.WriteLine("Rollback done!"); }
Console.WriteLine("After in RAM: " + f.ToString());
ctx.Entry(f).Reload();
Console.WriteLine("After in DB: " + f.ToString());
}
}
}
例 2
以下示例显示了对表Booking
( insert a new booking
)和表Flight
(减少自由选择的数量)进行更改的事务。这里,事务通过一个上下文类的两个不同的上下文实例发生。如果两个不同的上下文类引用同一个数据库,也可以通过它们进行事务处理。
请注意以下几点:
- 数据库连接是单独创建和打开的。
- 该事务在此连接上打开。
- 上下文实例不打开自己的连接,而是使用打开的连接。为此,数据库连接对象被传递到上下文类的构造函数中,并保存在那里。在
OnConfiguring()
中,这个数据库连接对象必须和UseSqlServer()
或者类似的一起使用,而不是将连接字符串作为参数传递! - 实例化后,事务对象必须被传递给
ctx.Database.UseTransaction()
。
Note
未能提前打开连接并将其传递给相关的上下文实例将导致以下运行时错误:“指定的事务与当前连接不相关联。只能使用与当前连接相关联的事务。
图 10-11 显示了输出。
图 10-11
Output of the previous example
public static void ExplicitTransactionTwoContextInstances()
{
CUI.MainHeadline(nameof(ExplicitTransactionTwoContextInstances));
// Open shared connection
using (var connection = new SqlConnection(Program.CONNSTRING))
{
connection.Open();
// Start transaction. Default is System.Data.IsolationLevel.ReadCommitted
using (var t = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
{
// Print isolation level
Console.WriteLine("Transaction with Level: " + t.IsolationLevel);
int flightNo;
using (var ctx = new WWWingsContext(connection))
{
ctx.Database.UseTransaction(t);
var all = ctx.FlightSet.ToList();
var flight = ctx.FlightSet.Find(111);
flightNo = flight.FlightNo;
ctx.Database.ExecuteSqlCommand("Delete from booking where flightno= " + flightNo);
var pasID = ctx.PassengerSet.FirstOrDefault().PersonID;
// Create and persist booking
var b = new BO.Booking();
b.FlightNo = flightNo;
b.PassengerID = pasID;
ctx.BookingSet.Add(b);
var count1 = ctx.SaveChanges();
Console.WriteLine("Numer of bookings saved: " + count1);
}
using (var ctx = new WWWingsContext(connection))
{
ctx.Database.UseTransaction(t);
// Change free seats and save
var f = ctx.FlightSet.Find(flightNo);
Console.WriteLine("BEFORE: " + f.ToString());
f.FreeSeats--;
f.Memo = "last changed at " + DateTime.Now.ToString();
Console.WriteLine("AFTER: " + f.ToString());
var count2 = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count2);
Console.WriteLine("Commit or Rollback? 1 = Commit, other = Rollback");
var input = Console.ReadKey().Key;
Console.WriteLine();
if (input == ConsoleKey.D1)
{t.Commit(); Console.WriteLine("Commit done!");}
else
{t.Rollback(); Console.WriteLine("Rollback done!");}
Console.WriteLine("After in RAM: " + f.ToString());
ctx.Entry(f).Reload();
Console.WriteLine("After in DB: " + f.ToString());
}
}
}
}
使用更改跟踪器
内置于实体框架核心中的变更跟踪器监视连接到实体框架核心上下文的所有对象的变更,可以通过程序代码在任何时候进行查询。
获取对象的状态
因为实体框架核心与普通的旧 CLR 对象(POCOs)一起工作,这些对象具有实体对象而不是基类,并且实现接口,所以实体对象不知道它们的上下文类或状态。
要查询对象状态,不要询问实体对象本身,而是询问上下文类的ChangeTracker
对象。ChangeTracker
对象有一个Entry()
方法,为给定的Entity
对象返回一个关联的EntryObject<EntityType>
。该对象拥有以下内容:
ChangeTracker
对象有一个EntityState
类型的State
属性,它是一个枚举类型,值为Added
、Deleted
、Detached
、Modified
和Unchanged
。- 在属性中,您可以找到一个以
PropertyEntry
对象形式的实体对象的所有属性的列表。每个PropertyEntry
对象都有一个IsModified
属性,指示属性是否被更改,以及旧值(OriginalValue
和新值(CurrentValue
)。 - 使用
EntryObject<EntityType>
,您也可以通过使用Property
方法指定一个 lambda 表达式来直接获得一个特定的PropertyEntry
对象。 GetDatabaseValues()
用于从数据库中获取对象的当前状态。
清单 10-10 中的子程序加载一个Flight
(第一个)并修改这个Flight
对象。在程序开始时,不仅为Flight
对象本身创建了一个变量,还为EntryObject<Flight>
创建了一个entryObj
变量,为PropertyEntry
对象创建了propObj
。
加载Flight
后,entryObj
和propObj
首先被ChangeTracker
对象的对象填充。实体对象处于Unchanged
状态,FreeSeats
属性返回IsModified False
。然后对象在属性FreeSeats
中被改变。实体对象现在处于Modified
状态,FreeSeats
的IsModified
返回True
。
Note
重要的是从上下文的ChangeTracker
对象中检索信息;EntryObject<Flight>
和PropertyEntry
的实例不会随着实体对象的改变而自动更新,而是反映了检索时的当前状态。
因此,您还必须在从ChangeTracker
对象调用SaveChanges()
方法后第三次请求这些对象。在SaveChanges()
之后,实体对象的状态再次变为Unchanged
,属性FreeSeats
返回IsModified False
。
该例程还循环遍历EntryObject<Flight>
的Properties
属性,以使用数据库的旧值和新值以及当前值返回实体对象的所有修改属性。该值可使用EntryObject<Flight>
中的GetDatabaseValues()
方法确定。然后GetDatabaseValues()
对数据库进行查询,并用数据库中的所有当前值填充一个PropertyValues
列表。数据库中的这些值可能与实体框架核心知道的值不同,并且在OriginalValue
属性中可见,另一个进程(或同一进程中的另一个实体框架核心上下文)同时保存了对记录的更改。在这种情况下,发生了数据冲突。图 10-12 显示了输出。
图 10-12
Output
/// </summary>
public static void ChangeTracking_OneObject()
{
CUI.MainHeadline(nameof(ChangeTracking_OneObject));
Flight flight;
EntityEntry<BO.Flight> entryObj;
PropertyEntry propObj;
using (var ctx = new WWWingsContext())
{
CUI.Headline("Loading Object...");
flight = (from y in ctx.FlightSet select y).FirstOrDefault();
// Access Change Tracker
entryObj = ctx.Entry(flight);
propObj = entryObj.Property(f => f.FreeSeats);
Console.WriteLine(" Object state: " + entryObj.State);
Console.WriteLine(" Is FreeSeats modified?: " + propObj.IsModified);
CUI.Headline("Changing Object...");
flight.FreeSeats--;
// Access Change Tracker again
entryObj = ctx.Entry(flight);
propObj = entryObj.Property(f => f.FreeSeats);
Console.WriteLine(" Object state: " + entryObj.State);
Console.WriteLine(" Is FreeSeats modified?: " + propObj.IsModified);
// Print old and new values
if (entryObj.State == EntityState.Modified)
{
foreach (PropertyEntry p in entryObj.Properties)
{
if (p.IsModified)
Console.WriteLine(" " + p.Metadata.Name + ": " + p.OriginalValue + "->" + p.CurrentValue +
" / State in database: " + entryObj.GetDatabaseValues()[p.Metadata.Name]);
}
}
CUI.Headline("Save...");
int count = ctx.SaveChanges();
Console.WriteLine(" Number of changes: " + count);
// Update of the Objects of the Change Tracker
entryObj = ctx.Entry(flight);
propObj = entryObj.Property(f => f.FreeSeats);
Console.WriteLine(" Object state: " + entryObj.State);
Console.WriteLine(" Is FreeSeats modified?: " + propObj.IsModified);
}
}
Listing 10-10Querying the Change Tracker for a Changed Object
列出所有已更改的对象
ChangeTracker
对象不仅可以提供单个对象的信息,还可以提供它使用其Entries()
方法监控的所有实体对象的列表。然后,您可以根据所需的状态过滤实体对象。
清单 10-11 中的例程修改三个航班,然后创建一个值为 123456 的航班(如果它还不存在的话)。如果Flight
对象已经存在,它将被删除。此后,例程分别向ChangeTracker
对象请求新的、已更改的和已删除的对象(清单 10-12 )。三套都是由Entries()
提供的。使用Where()
操作符将集合从 LINQ 过滤到对象。在这三种情况下,都会调用PrintChangedProperties()
助手例程。但是只有在对象改变的情况下,它才提供一些输出。如果对象已被添加或删除,则各个属性被视为未更改。
图 10-13 和图 10-14 显示输出。
public static void ChangeTracking_MultipleObjects()
{
CUI.MainHeadline(nameof(ChangeTracking_MultipleObjects));
using (var ctx = new WWWingsContext())
{
var flightQuery = (from y in ctx.FlightSet select y).OrderBy(f4 => f4.FlightNo).Take(3);
foreach (var flight in flightQuery.ToList())
{
flight.FreeSeats -= 2;
flight.Memo = "Changed on " + DateTime.Now;
}
var newFlight = ctx.FlightSet.Find(123456);
if (newFlight != null)
{
ctx.Remove(newFlight);
}
else
{
newFlight = new Flight();
newFlight.FlightNo = 123456;
newFlight.Departure = "Essen";
newFlight.Destination = "Sydney";
newFlight.AirlineCode = "WWW";
newFlight.PilotId = ctx.PilotSet.FirstOrDefault().PersonID;
newFlight.Seats = 100;
newFlight.FreeSeats = 100;
ctx.FlightSet.Add(newFlight);
}
CUI.Headline("New objects");
IEnumerable<EntityEntry> neueObjecte = ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Added);
if (neueObjecte.Count() == 0) Console.WriteLine("none");
foreach (EntityEntry entry in neueObjecte)
{
CUI.Print("Object " + entry.Entity.ToString() + " State: " + entry.State, ConsoleColor.Cyan);
ITVisions.EFCore.EFC_Util.PrintChangedProperties(entry);
}
CUI.Headline("Changed objects");
IEnumerable<EntityEntry> geaenderteObjecte =
ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified);
if (geaenderteObjecte.Count() == 0) Console.WriteLine("none");
foreach (EntityEntry entry in geaenderteObjecte)
{
CUI.Print("Object " + entry.Entity.ToString() + " State: " + entry.State, ConsoleColor.Cyan);
ITVisions.EFCore.EFC_Util.PrintChangedProperties(entry);
}
CUI.Headline("Deleted objects");
IEnumerable<EntityEntry> geloeschteObjecte = ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Deleted);
if (geloeschteObjecte.Count() == 0) Console.WriteLine("none");
foreach (EntityEntry entry in geloeschteObjecte)
{
CUI.Print("Object " + entry.Entity.ToString() + " State: " + entry.State, ConsoleColor.Cyan);
}
Console.WriteLine("Changes: " + ctx.SaveChanges());
}
}
Listing 10-11Querying the Change Tracker for Several Changed Objects
图 10-14
Second pass of the code: flight 123456 is deleted again
图 10-13
First run of the code: flight 123456 is added
/// <summary>
/// Lists the changed properties of an object, including the current database state
/// </summary>
/// <param name="entry"></param>
public static void PrintChangedProperties(EntityEntry entry)
{
PropertyValues dbValue = entry.GetDatabaseValues();
foreach (PropertyEntry prop in entry.Properties.Where(x => x.IsModified))
{
var s = "- " + prop.Metadata.Name + ": " +
prop.OriginalValue + "->" +
prop.CurrentValue +
" State in the database: " + dbValue[prop.Metadata.Name];
Console.WriteLine(s);
}
}
Listing 10-12Auxiliary Routine for Querying the Change Tracker
十一、防止冲突(并发)
在许多生产场景中,多个人或自动后台任务可能同时访问相同的记录。这可能会导致冲突,发生相互矛盾的数据更改。本章展示了如何在实体框架核心中检测和解决这种冲突。
看一下并发的历史
与其前身实体框架和底层基本技术 ADO.NET 一样,实体框架核心不支持阻止其他进程的数据记录读取访问。这是微软在 2005 年的一个深思熟虑的决定。NET 1.0 (2002),因为锁会导致很多性能问题。在的 alpha 版本中。NET 2.0 (2005)中,在当时的新类SqlResultSet
中有这样一个锁函数的原型,但是这个类从未在. NET 的 RTM 版本中发布。
因此,在。NET 以及基于它的框架,比如实体框架,实体框架核心,只有所谓的乐观锁定。乐观锁定是一种委婉的说法,因为实际上在数据库管理系统和 RAM 中没有任何东西被阻塞。只能确保以后会注意到变更冲突。第一个想写更改的进程获胜。所有其他进程都无法写入,并将收到一条错误消息。为了实现这一点,WHERE
条件中的UPDATE
和DELETE
命令包含来自源记录的单个或多个值。
一个DataSet
连同一个DataAdapter
和一个CommandBuilder
对象不仅查询一个UPDATE
或DELETE
命令的WHERE
子句中的一个或多个主键列,而且还从进程在读取记录时接收的当前进程值的角度查询所有具有旧值的列(参见清单 11-1 )。同时,如果另一个进程更改了任何单独的列,UPDATE
或DELETE
命令不会在数据库管理系统中导致运行时错误;相反,它导致零个记录受到影响。这允许DataAdapter
检测到存在变更冲突。
UPDATE [dbo]. [Flight]
SET [FlightNo] = @p1, [Departure] = @p2, [Strikebound] = @p3, [CopilotId] = @p4, [FlightDate] = @p5, [Flightgesellschaft] = @p6, [AircraftTypeID] = @p7, [FreeSeats] = @p8, [LastChange] = @p9, [Memo] = @p10, [NonSmokingFlight] = @p11, [PilotId] = @p12, [Seats] = @p13, [Price] = @p14, [Timestamp] = @p15, [destination] = @p16
WHERE (([FlightNo] = @p17) AND ((@p18 = 1 AND [Departure] IS NULL) OR ([Departure] = @p19)) AND ((@p20 = 1 AND [Expires] IS NULL) OR ( [Strikebound] = @p21)) AND ((@p22 = 1 AND [CopilotId] IS NULL) OR ([CopilotId] = @p23)) AND ([FlightDate] = @p24) AND ([Airline] = @p25) AND ((@p26 = 1 AND [aircraftID_ID] IS NULL) OR ([aircraft_type_ID] = @p27)) AND ((@p28 = 1 AND [FreeSeats] IS NULL) OR ([FreeSeats] = @p29)) AND ( [Lastchange] = @p30) AND ((@p31 = 1 AND [NonSmokingFlight] IS NULL) OR ([NonSmokingFlight] = @p32)) AND ([PilotId] = @p33) AND ([Seats] = @p34) AND ((@p35 = 1 AND [price] IS NULL) OR ([price] = @p36)) AND ((@p37 = 1 AND [destination] IS NULL) OR ([destination] = @p38)))
Listing 11-1Update Command, As Created by a SqlCommandBuilder for the Flight Table with the Primary Key FlightNo
默认情况下没有冲突检测
实体框架核心和实体框架一样,默认情况下根本不锁,即使使用乐观锁也不行。标准很简单“最后写的人赢。”清单 11-2 展示了如何在 RAM 中更改一个Flight
对象,并通过实体框架核心用SaveChanges()
持久化该更改。该程序代码向数据库管理系统发送以下 SQL 命令:
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1;
SELECT @@ ROWCOUNT;
可以看到,WHERE
条件中只出现了主键FlightNo
;列FreeSeats
或其他列的旧值不出现。因此,该值是持久的,即使其他进程同时更改了该值。因此,航空公司可能会出现航班超额预订的情况。例如,如果只剩下两个空位,而两个进程(几乎)同时加载该信息,则这两个进程中的每一个都可以从剩余的空位中减去两个位置。然后,数据库中列FreeSeats
中的状态为零。事实上,四名乘客被安排在两个座位上。那在飞机上会很紧!
虽然SaveChanges()
打开了一个事务,但它仅适用于一个存储操作,因此不能防止数据更改冲突。然而,忽略冲突对于用户来说通常是不可行或不可接受的。幸运的是,您可以重新配置实体框架核心,就像您在它的前身实体框架中首先处理代码一样。
实体框架核心会注意到的唯一更改冲突是用另一个进程删除记录,因为在这种情况下,UPDATE
命令会返回零个记录被更改的事实,然后实体框架核心会引发一个DbUpdateConcurrencyException
错误。
public static void Change.FlightOneProperty()
{
CUI.MainHeadline(nameof(ChangeFlightOneProperty));
int FlightNr = 101;
using (WWWingsContext ctx = new WWWingsContext())
{
// Load flight
var f = ctx.FlightSet.Find(FlightNr);
Console.WriteLine($"Before changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Change object in RAM
f.FreeSeats -= 2;
Console.WriteLine($"After changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Persist changes
try
{
var count = ctx.SaveChanges();
if (count == 0)
{
Console.WriteLine("Problem: No changes saved!");
}
else
{
Console.WriteLine("Number of saved changes: " + count);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! Zustand des Flight-Objekts: " + ctx.Entry(f).State);
}
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.ToString());
}
}
}
Listing 11-2Changing a Flight Object
使用乐观锁定检测冲突
实体框架核心向数据库管理系统发送以下 SQL 命令:
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1 AND [FreeSeats] = @p2;
SELECT @@ROWCOUNT;
这里,FlightNo
除了查询主键之外,还对FreeSeats
列的旧值(读取时的原始值)进行查询。为了实现这种冲突检测,需要改变的不是程序代码,而是实体框架核心模型。
有两种方法可以配置模型。
- 通过数据注释
[ConcurrencyCheck]
- 通过 Fluent API 中的
IsConcurrencyToken()
清单 11-3 显示了实体类Flight
的一个部分。这里,FreeSeats
用[ConcurrencyCheck]
进行了注释,实体框架核心自动查询所有UPDATE
和DELETE
命令的WHERE
条件中的旧值。这是通过在实体框架核心上下文类的OnModelCreating()
中的相应PropertyBuilder
对象上调用IsConcurrencyToken()
来实现的(参见清单 11-4 )。
public class Flight
{
[Key]
public int FlightNo {get; set; }
[ConcurrencyCheck]
public short? FreeSeats {get; set;}
[ConcurrencyCheck]
public decimal? Price {get; set; }
public short? Seats { get; set; }
...
}
Listing 11-3Use of Data Annotation [ConcurrencyCheck]
public class WWWingsContext: DbContext
{
public DbSet<Flight> FlightSet { get; set; }
...
protected override void OnModelCreating (ModelBuilder builder)
{
Builder %Entity<Flight>().Property (f => f.FreeSeats).IsConcurrencyToken();
...
}
}
Listing 11-4Using IsConcurrencyToken( ) in the Fluent API
现在,对几个列运行冲突检查可能是有用的。例如,冲突检查也可以通过Flight
对象的Price
列来执行。就内容而言,这意味着如果Flight
的价格已经改变,你不能改变座位的数量,因为这个预订将会以旧价格显示给用户。然后,您可以用[ConcurrencyCheck
注释Price
属性,或者将其添加到 Fluent API 中。
builder.Entity<Flight>().Property(x => x.FreeSeats).ConcurrencyToken();
下面的 SQL 命令在WHERE
条件中包含三个部分,来自清单:
SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1 AND [FreeSeats] = @p2 AND [Price] = @p3;
SELECT @@ ROWCOUNT;
检测所有属性的冲突
对于所有实体类和所有持久属性,通过数据注释或 Fluent API 进行这种配置可能会很乏味。幸运的是,Entity Framework Core 允许您进行大量配置。清单 11-5 展示了如何从ModelBuilder
对象中使用OnModelCreating()
中的Model
。GetEntityTypes()
通过GetProperties()
获取所有实体类的列表以及每个实体类中的所有属性,从而在那里设置IsConcurrencyToken = true
。
public class WWWingsContext: DbContext
{
public DbSet<Flight> FlightSet { get; set; }
...
protected override void OnModelCreating (ModelBuilder builder)
{
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
// get all properties
foreach (var prop in entity.GetProperties())
{
prop.IsConcurrencyToken = true;
}
}
...
}
}
Listing 11-5Mass Configuration of the ConcurrencyToken for All Properties in All Entity Classes
然后,清单创建了一个 SQL 命令,其中包含了WHERE
条件中的所有列,如下所示:
SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1 AND [Departure] = @p2 AND [Destination] = @p3 AND [CopilotId] = @p4 AND [FlightDate] = @p5 AND [Airline] = @p6 AND [AircraftTypeID] IS NULL AND [ FreeSeats] = @p7 AND [LastChange] = @p8 AND [Memo] = @p9 AND [NonSmokingFlight] IS NULL AND [PilotId] = @p10 AND [Seats] = @p11 AND [Price] = @p12 AND [Strikebound] = @p13;
SELECT @@ ROWCOUNT;
通过惯例解决冲突
如果您想排除个别列,这也是可能的。在这种情况下,定义一个单独的注释是有意义的,称为[ConcurrencyNoCheckAttribute]
(参见清单 11-6 ),然后注释实体类的所有持久属性,实体框架核心不应该对这些属性执行冲突检查。清单 11-7 显示了考虑注释[ConcurrencyNoCheck]
的示例的扩展。这里重要的是PropertyInfo
后的零传播算子?.
;这很重要,因为您可以在实体框架核心中定义所谓的影子属性,这些属性只存在于实体框架核心模型中,而不存在于实体类中。这些 shadow 属性没有PropertyInfo
对象,所以在 shadow 属性没有空传播操作符的情况下,会出现常见的Null Reference
运行时错误。使用ConcurrencyNoCheckAttribute
,您可以根据需要从冲突检查中优雅地排除单个属性。
using system;
namespace EFCExtensions
{
/// <summary>
/// Annotation for EFCore entity classes and properties for which EFCore should not run a concurrency check
/// </ summary>
[AttributeUsage (AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)]
public class ConcurrencyNoCheckAttribute: Attributes
{
}
}
Listing 11-6Annotation for Entity Class Properties for Which Entity Framework Core Should Not Run a Concurrency Check
public class WWWingsContext: DbContext
{
public DbSet<Flight> FlightSet {get; set; }
...
protected override void OnModelCreating (ModelBuilder builder)
{
// Get all entity classes
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
// get all properties
foreach (var prop in entity.GetProperties())
{
// Look for annotation [ConcurrencyNoCheck]
var annotation = prop.PropertyInfo?.GetCustomAttribute<ConcurrencyNoCheckAttribute>();
if (annotation == null)
{
prop.IsConcurrencyToken = true;
}
else
{
Console.WriteLine("No Concurrency Check for" + prop.Name);
}
if (prop.Name == "Timestamp")
{
prop.ValueGenerated = ValueGenerated.OnAddOrUpdate;
prop.IsConcurrencyToken = true;
}
foreach (var a in prop.GetAnnotations())
{
Console.WriteLine(prop.Name + ":" + a.Name + "=" + a.Value);
}
}
}
}
...
}
}
Listing 11-7Mass Configuration of the ConcurrencyToken for All Properties in All Entity Classes, Except the Properties Annotated with [ConcurrencyNoCheck]
单独设置冲突检查
有时,在实践中,希望在逐案例例的基础上为各个属性的各个更改激活或停用冲突检查。不幸的是,这无法实现,因为数据注释是编译的,而且每个进程只调用一次OnModelCreating()
。遗憾的是,在OnModelCreating()
结束后,您无法更改实体框架核心模型。虽然DbContext
类像ModelBuilder
类一样提供了属性模型,但是在ModelBuilder
中,属性模型具有IMutalModel
类型(顾名思义,这是一个变量)。DbContext
只获取IModel
类型,而IsConcurrencyToken
像许多其他属性一样是只读的。因此,如果您想逐个更改乐观锁定列,您需要自己向数据库管理系统发送UPDATE
和DELETE
命令(通过实体框架核心或其他方式)。
添加时间戳
可以引入一个额外的时间戳列,而不是在单个数据列级别进行原始值比较。您可以在 Microsoft SQL Server 中找到一个这样的列,类型为rowversion
( https://docs.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql
),称为timestamp
(参见图 11-1 和图 11-2 )。对于每个单独的数据记录变化,它由数据库管理系统自动增加。因此,在使用UPDATE
或DELETE
命令的情况下,只需检查该值是否仍为加载期间存在的先前值。如果是这样,整个记录保持不变。如果没有,则另一个进程至少更改了部分记录。然而,使用timestamp
列,您无法区分变更相关的列和变更不相关的列。数据库管理系统在每次列改变时调整时间戳;不可能有例外。
Note
虽然目前 SQL Server Management Studio (SSMS)仍然显示旧名称timestamp
,但是 Visual Studio 2016 中的 SQL Server Data Tools 显示了当前名称rowversion
。
图 11-1
A timestamp column for a record in Microsoft SQL Server 2017 as shown in Visual Studio 2017
图 11-2
A timestamp column for a record in Microsoft SQL Server 2017 as shown in SQL Server Management Studio 17.1
要使用时间戳进行变更冲突检测,您需要向实体类添加一个byte
-数组列(byte[]
),并用[timestamp]
对其进行注释。然而,列的名称与实体框架核心无关。
[Timestamp]
public byte[] Timestamp { get; set; }
或者,您可以使用 Fluent API 再次进行设置,但这发生在程序代码中,如下所示:
builder.Entity<Flight>()
.Property(p => p.Timestamp)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
从实体框架核心 1.1 版本开始,也可以使用IsRowVersion()
作为替代,如下图:
modelBuilder.Entity<Flight>().Property(x => x.Timestamp).IsRowVersion();
Note
每个表只能有一个timestamp
/ rowversion
列。不幸的是,错误消息“一个表只能有一个时间戳列。”仅在您调用Update-Database
时发生,不与Add-Migration
一起发生。
对于时间戳支持,您不需要实现其他任何东西。如果在对象模型中有这样的属性,并且在数据库表中有相应的列,那么对于WHERE
条件中的所有DELETE
和UPDATE
命令,实体框架核心总是引用先前的时间戳值。
SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1 AND [Timestamp] IS NULL;
SELECT [Timestamp]
FROM [Flight]
WHERE @@ ROWCOUNT = 1 AND [FlightNo] = @p1;
如您所见,Entity Framework Core 还使用SELECT [Timestamp]
来重新加载数据库管理系统在UPDATE
之后更改的时间戳,以相应地更新 RAM 中的对象。如果没有发生这种情况,那么对象的第二次更新将是不可能的,因为这样 RAM 中的时间戳将会过时,并且即使没有任何更改冲突,Entity Framework Core 也将总是报告更改冲突(因为第一次更改是更改数据库表中时间戳的那个更改)。
按照惯例,时间戳配置也可以自动化。清单 11-8 中所示的批量配置自动为所有带有名称timestamp
的属性添加时间戳,用于冲突检测。
public class WWWingsContext: DbContext
{
public DbSet<Flight> FlightSet {get; set; }
...
protected override void OnModelCreating (ModelBuilder builder)
{
// Get all entity classes
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
// Get all properties
foreach (var prop in entity.GetProperties())
{
if (prop.Name == "Timestamp")
{
prop.ValueGenerated = ValueGenerated.OnAddOrUpdate;
prop.IsConcurrencyToken = true;
}
}
}
...
}
}
Listing 11-8Automatically Turning Any Properties Called Timestamp into Timestamps for Conflict Detection
解决冲突
本节说明如何验证冲突检测。图 11-3 显示了程序两次启动时的一些典型输出。首先启动 ID 为 10596 的进程,然后启动进程 18120。两人都读到了编号为 101 的航班,该航班目前还有 143 个座位。然后,过程 10596 将位置数减少 5,并在FreeSeats
列中保持 138。现在进程 18120 为两个人预订了一个房间,所以在 RAM 中该值变为 141 FreeSeats
。然而,进程 18120 不能持续,因为实体框架核心由于对FreeSeats
列的冲突检测或基于时间戳列抛出了类型为DbUpdateConcurrencyException
的错误。过程 18120 中的用户被给予接受或覆盖其他用户的改变或者抵消这两个改变的选择,这在某些情况下可能是有意义的。
图 11-3
Conflict detection and conflict resolution
清单 11-9 显示了实现。SaveChanges()
接住了DbUpdateConcurrencyException
。在错误处理程序中,PrintChangedProperties()
助手函数用于指定在此过程中航班的哪些属性发生了更改,以及当前的数据库状态是什么。您通过方法GetDatabaseValues()
获得数据库的当前状态,该方法向数据库管理系统发送相应的 SQL 查询。之后,用户必须做出决定。如果用户选择将更改应用到另一个进程,则在实体框架核心 API 中调用Reload()
方法,该方法丢弃 RAM 中已更改的对象,并从数据库中重新加载它。如果用户选择覆盖对其他进程的更改,程序代码会稍微复杂和间接一些。命令链从数据库加载当前状态,并在实体框架核心变更跟踪器中将其设置为对象的原始值:ctx.Entry(Flight).OriginalValues.SetValues(ctx.Entry(Flight).GetDatabaseValues())
。之后,SaveChanges()
再次被调用,现在它可以工作了,因为在WHERE
条件中使用的原始值或时间戳对应于数据库中数据记录的当前状态。然而,从理论上讲,如果在GetDatabaseValues()
和SaveChanges()
之间的短时间内,另一个进程更改了数据库表中的记录,冲突可能会再次发生。因此,您应该封装SaveChanges()
和相关的错误处理,但是为了更好地说明这个例子,这里没有这样做。
在图 11-3 中,用户选择了第三个选项,清除了两个变更。除了它的原始值和当前值,过程 18120 还需要列FreeSeats
的当前数据库值。结果是正确的 136。但是,计算假设两个过程都从相同的原始值开始。如果没有进程间通信,进程 18120 可能不知道进程 10596 的种子值。账单只在特殊情况下有效。
当然,也可以让用户在(图形)用户界面中通过输入值而不是决定哪一方来解决冲突。就实现而言,清除就像另一个值的输入一样,对应于第二种情况,换句话说,覆盖另一个过程的更改。在调用SaveChanges()
之前,简单地在对象中设置你想要随后在数据库中拥有的值(参见清单 11-9 和清单 11-10 中的案例ConsoleKey.D3
)。
public static void ConflictWhileChangingFlight()
{
CUI.MainHeadline(nameof(ConflictWhileChangingFlight));
Console.WriteLine("Process.ID=" + Process.GetCurrentProcess().Id);
Console.Title = nameof(ConflictWhileChangingFlight) + ": Process-ID=" + Process.GetCurrentProcess().Id;
// Flight, where the conflict should arise
int flightNo = 151;
using (WWWingsContext ctx = new WWWingsContext())
{
// --- load flight
Flight flight = ctx.FlightSet.Find(flightNo);
Console.WriteLine(DateTime.Now.ToLongTimeString() + ": free seats Before: " + flight.FreeSeats);
short seats = 0;
string input = "";
do
{
Console.WriteLine("How many seats do you need at this flight?");
input = Console.ReadLine(); // wait (time to start another process)
} while (!Int16.TryParse(input, out seats));
// --- change the free seats
flight.FreeSeats -= seats;
Console.WriteLine(DateTime.Now.ToLongTimeString() + ": free seats NEW: " + flight.FreeSeats);
try
{
// --- try to save
EFC_Util.PrintChangedProperties(ctx.Entry(flight));
var count = ctx.SaveChanges();
Console.WriteLine("SaveChanges: Number of saved changes: " + count);
}
catch (DbUpdateConcurrencyException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
CUI.PrintError(DateTime.Now.ToLongTimeString() + ": Error: Another user has already changed the flight!");
CUI.Print("Conflicts with the following properties:");
EFC_Util.PrintChangedProperties(ex.Entries.Single());
// --- Ask the user
Console.WriteLine("What do you want to do?");
Console.WriteLine("Key 1: Accept the values of the other user");
Console.WriteLine("Key 2: Override the values of the other user");
Console.WriteLine("Key 3: Calculate new value from both records");
ConsoleKeyInfo key = Console.ReadKey();
switch(key.Key)
{
case ConsoleKey.D1: // Accept the values of the other user
{
Console.WriteLine("You have chosen: Option 1: Accept");
ctx.Entry(flight).Reload();
break;
}
case ConsoleKey.D2: // Override the values of the other user
{
Console.WriteLine("You have chosen: Option 2: Override");
ctx.Entry(flight).OriginalValues.SetValues(ctx.Entry(flight).GetDatabaseValues());
// wie RefreshMode.ClientWins bei ObjectContext
EFC_Util.PrintChangeInfo(ctx);
int count = ctx.SaveChanges();
Console.WriteLine("SaveChanges: Saved changes: " + count);
break;
}
case ConsoleKey.D3: // Calculate new value from both records
{
Console.WriteLine("You have chosen: Option 3: Calculate");
var FreeSeatsOrginal = ctx.Entry(flight).OriginalValues.GetValue<short?>("FreeSeats");
var FreeSeatsNun = flight.FreeSeats.Value;
var FreeSeatsInDB = ctx.Entry(flight).GetDatabaseValues().GetValue<short?>("FreeSeats");
flight.FreeSeats = (short) (FreeSeatsOrginal -
(FreeSeatsOrginal - FreeSeatsNun) -
(FreeSeatsOrginal - FreeSeatsInDB));
EFC_Util.PrintChangeInfo(ctx);
ctx.Entry(flight).OriginalValues.SetValues(ctx.Entry(flight).GetDatabaseValues());
int count = ctx.SaveChanges();
Console.WriteLine("SaveChanges: Saved changes: " + count);
break;
}
}
}
Console.WriteLine(DateTime.Now.ToLongTimeString() + ": free seats after: " + flight.FreeSeats);
// --- Cross check the final state in the database
using (WWWingsContext ctx2 = new WWWingsContext())
{
var f = ctx.FlightSet.Where(x => x.FlightNo == flightNo).SingleOrDefault();
Console.WriteLine(DateTime.Now.ToLongTimeString() + ": free seats cross check: " + f.FreeSeats);
} // End using-Block -> Dispose()
}
}
Listing 11-9Conflict Detection and Conflict Resolution with Entity Framework Core
/// <summary>
/// Print all changed objects and the changed properties
/// </summary>
/// <param name="ctx"></param>
public static void PrintChangeInfo(DbContext ctx)
{
foreach (EntityEntry entry in ctx.ChangeTracker.Entries())
{
if (entry.State == EntityState.Modified)
{
CUI.Print(entry.Entity.ToString() + " Object state: " + entry.State, ConsoleColor.Yellow);
IReadOnlyList<IProperty> listProp = entry.OriginalValues.Properties;
PrintChangedProperties(entry);
}
}
}
/// <summary>
/// Print the changed properties of an object, including the current database state
/// </summary>
/// <param name="entry"></param>
public static void PrintChangedProperties(EntityEntry entry)
{
PropertyValues dbValue = entry.GetDatabaseValues();
foreach (PropertyEntry prop in entry.Properties.Where(x => x.IsModified))
{
var s = "- " + prop.Metadata.Name + ": " +
prop.OriginalValue + "->" +
prop.CurrentValue +
" State in the database: " + dbValue[prop.Metadata.Name];
Console.WriteLine(s);
}
}
Listing 11-10Subroutines for Listing 11-9
实体框架核心上的悲观锁定
虽然微软故意没有在。NET 和。NET Core 可以用来阻止其他人对记录的读访问,但是我经常遇到一些客户,他们仍然迫切地想从一开始就避免冲突。使用 LINQ 命令,即使激活了事务,读锁也是不可行的。您需要一个事务和一个特定于数据库管理系统的 SQL 命令。在 Microsoft SQL Server 中,这是与事务相关联的查询提示SELECT ... WITH (UPDLOCK)
。这个查询提示确保读记录被锁定,直到事务完成。它只在一个事务中工作,所以你会在清单 11-11 中找到一个ctx.Database.BeginTransaction()
方法,然后是对commit()
的调用。清单还展示了 Entity Framework Core 提供的FromSql()
方法的使用,它允许您将自己的 SQL 命令发送到数据库管理系统,并将结果具体化为实体对象。
public static void UpdateWithReadLock()
{
CUI.MainHeadline(nameof(UpdateWithReadLock));
Console.WriteLine("--- Change flight");
int flightNo = 101;
using (WWWingsContext ctx = new WWWingsContext())
{
try
{
ctx.Database.SetCommandTimeout(10); // 10 seconds
// Start transaction
IDbContextTransaction t = ctx.Database.BeginTransaction(IsolationLevel.ReadUncommitted); // default is System.Data.IsolationLevel.ReadCommitted
Console.WriteLine("Transaction with Level: " + t.GetDbTransaction().IsolationLevel);
// Load flight with read lock using WITH (UPDLOCK)
Console.WriteLine("Load flight using SQL...");
Flight f = ctx.FlightSet.FromSql("SELECT * FROM dbo.Flight WITH (UPDLOCK) WHERE flightNo = {0}", flightNo).SingleOrDefault();
Console.WriteLine($"Before changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
Console.WriteLine("Waiting for ENTER key...");
Console.ReadLine();
// Change object in RAM
Console.WriteLine("Change flight...");
f.FreeSeats -= 2;
Console.WriteLine($"After changes: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
// Send changes to DBMS
Console.WriteLine("Save changes...");
var c = ctx.SaveChanges();
t.Commit();
if (c == 0)
{
Console.WriteLine("Problem: No changes saved!");
}
else
{
Console.WriteLine("Number of saved changes: " + c);
Console.WriteLine($"After saving: Flight #{f.FlightNo}: {f.Departure}->{f.Destination} has {f.FreeSeats} free seats! State of the flight object: " + ctx.Entry(f).State);
}
}
catch (Exception ex)
{
CUI.PrintError("Error: " + ex.ToString());
}
}
}
Listing 11-11A Lock Is Already Set Up When Reading the Data Record
图 11-4 提供了事实上只要第一个进程还没有完成它的事务,第二个进程就不能读取FlightNo
101 的证据。在这个示例代码中,处理在事务中间等待用户输入。事务中的用户输入当然是“最糟糕的实践”,不应该出现在生产代码中。然而,在示例代码中,它是一个有用的工具,可以在几秒钟内模拟一个事务的运行时,直到另一个进程超时。
图 11-4
If the program code runs twice in parallel, in the second run the process will time out because the first process has a read lock on the record Note
我必须重申,数据库管理系统中的这种数据记录锁不是一种好的做法。锁,尤其是读锁,会降低应用的速度,还会很快导致死锁,即进程相互等待,从而无法再进行处理。这种做法损害了软件的性能、可伸缩性和稳定性。为什么我要在这一章展示它?因为我知道有些开发商还是要的。
顺便说一句,在数据库管理系统中记录锁的一个更好的替代方法是在应用级别使用锁,其中应用管理锁,也可能使用 RAM 中的自定义锁表。这样做的好处是,应用可以准确地呈现给当前正在处理记录的用户。比如用户 Müller 可以说“这个记录还有 4 分 29 秒由你独家编辑。”迈耶夫人说,“米勒先生正在处理这个数据集。他还有 4 分 29 秒来保存记录。在此期间,您不能对此记录进行任何更改。这提供了更多的可能性。例如,您可以让按钮显示“我是明星,现在我想立即将米勒先生从唱片中踢出去。”然而,在实体框架核心中,没有预定义的应用级锁定机制。这里需要你自己的创造力!
十二、日志
在传统的实体框架中,有两种简单的方法来获取或映射器发送给数据库的 SQL 命令。
- 可以对查询对象(
IQueryable<T>
)调用ToString()
。 - 您可以使用
Log
属性(从实体框架 6.0 版开始),就像在ctx.Database.Log = Console.WriteLine;
中一样。
不幸的是,这两个选项在实体框架核心中都不可用。
以下命令
var query = ctx.FlightSet.Where(x => x.FlightNo > 300).OrderBy(x => x.- Date).Skip(10).Take(5);
Console.WriteLine (query.ToString());
仅提供以下输出:Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable
1 [BO.Flight]`。
ctx.Database
对象在实体框架核心中没有log
属性。
使用扩展方法 Log()
登录实体框架核心是可能的,但是比它的前身要复杂得多。因此,我以DbContext
类的Log()
扩展方法的形式创建了对Database
对象的扩展。它是一个方法,而不是属性,因为在。不幸的是,只有扩展方法,没有扩展属性。
Note
本书的一些清单中使用了Log()
方法。
你可以像这样使用方法Log()
:
using (var ctx1 = new WWWingsContext())
{
var query1 = ctx1.FlightSet.Where(x => x.FlightNo > 100).OrderBy(x => x.Date).Skip(10).Take(5);
ctx1.Log(Console.WriteLine);
var flightSet1 = query1.ToList();
flightSet1.ElementAt(0).FreeSeats--;
ctx1.SaveChanges();
}
类似于实体框架中的Log
属性,Log()
是一个没有返回值的方法;它需要一个字符串作为唯一的参数。与经典的实体框架不同,您可以省略Log()
中的参数。然后输出会自动打印到青色的Console.WriteLine()
(见图 12-1 )。
图 12-1
Default logging for the Log( ) method
using (var ctx2 = new WWWingsContext())
{
var query2 = ctx2.FlightSet.Where(x => x.FlightNo < 3000).OrderBy(x => x.Date).Skip(10).Take(5);
ctx2.Log();
var flightSet2 = query2.ToList();
flightSet2.ElementAt(0).FreeSeats--;
ctx2.SaveChanges();
}
如果您想记录到一个文件,您可以通过编写一个带有字符串参数且没有返回值的方法,并将其传递给Log()
来实现。
using (var ctx3 = new WWWingsContext())
{
Console.WriteLine("Get some flights...");
var query3 = ctx3.FlightSet.Where(x => x.FlightNo > 100).OrderBy(x => x.Date).Skip(10).Take(5);
ctx3.Log(LogToFile);
var flightSet3 = query3.ToList();
flightSet3.ElementAt(0).FreeSeats--;
ctx3.SaveChanges();
}
}
public static void LogToFile(string s)
{
Console.WriteLine(s);
var sw = new StreamWriter(@"c:\temp\log.txt");
sw.WriteLine(DateTime.Now + ": " + s);
sw.Close();
}
默认情况下,Log()
方法只记录那些发送到 DBMS 的命令。Log()
方法的另外两个参数影响日志记录的数量,如下所示:
- 参数 2 是日志类别(字符串)的列表。
- 参数 3 是事件编号(数字)的列表。
下一个命令将打印来自实体框架核心的所有日志输出(它为每个命令生成大量屏幕输出):
ctx1.Log(Console.WriteLine, new List<string>(), new List<int>());
下一个命令将只打印某些日志类别和事件号:
ctx1.Log(Console.WriteLine, new List<string>() { "Microsoft.EntityFrameworkCore.Database.Command" }, new List<int>() { 20100, 20101});
事件 20100 是Executing
,20101 是Executed
。
Note
因为实体框架核心不是将内部使用的记录器工厂类分配给一个上下文实例,而是分配给所有上下文实例,所以在特定实例上建立的日志记录方法也适用于同一上下文类的其他实例。
实现 Log()扩展方法
列表 12-1 展示了扩展方法Log()
的实现。
Log()
扩展方法向ILoggerFactory
服务添加一个记录器提供者的实例。- logger provider 是一个实现
ILoggerProvider
的类。在这个类中,实体框架核心为每个日志记录类别调用一次CreateLogger()
。 CreateLogger()
然后必须为每个记录类别提供一个记录器实例。- 记录器是一个实现
ILogger
的类。 - 清单 12-1 有一个
FlexLogger
类,它向Log()
指定的方法发送一个字符串。如果没有指定方法,则调用ConsoleWriteLineColor()
。 - 第二个 logger 类是
NullLogger
,它丢弃与 SQL 输出无关的所有日志类别的日志输出。
// Logging for EF Core
// (C) Dr. Holger Schwichtenberg, www.IT-Visions.de 2016-2017
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace ITVisions.EFCore
{
/// <summary>
/// Enhancement for the DbContext class for easy logging of the SQL commands sent by EF Core to a method that expects a string (C) Dr. Holger Schwichtenberg, www.IT-Visions.de
/// </summary>
public static class DbContextExtensionLogging
{
public static Dictionary<string, ILoggerProvider> loggerFactories = new Dictionary<string, ILoggerProvider>();
public static bool DoLogging = true;
public static bool DoVerbose = true;
private static Version VERSION = new Version(4, 0, 0);
private static List<string> DefaultCategories = new List<string>
{
"Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory", // für EFCore 1.x
"Microsoft.EntityFrameworkCore.Database.Sql", // für EFCore 2.0Preview1
"Microsoft.EntityFrameworkCore.Database.Command", // für EFCore >= 2.0Preview2
};
private static List<int> DefaultEventIDs = new List<int>
{
20100 // 20100 = "Executing"
};
public static void ClearLog(this DbContext ctx)
{
var serviceProvider = ctx.GetInfrastructure<IServiceProvider>();
// Add Logger-Factory
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
(loggerFactory as LoggerFactory).Dispose();
}
/// <summary>
/// Extension Method for Logging to a method expecting a string
/// </summary>
/// <example>Log() or Log(Console.WriteLine) for console logging</example>
public static void Log(this DbContext ctx, Action<string> logMethod = null, List<string> categories = null, List<int> eventsIDs = null, bool verbose = false)
{
DbContextExtensionLogging.DoVerbose = verbose;
if (eventsIDs == null) eventsIDs = DefaultEventIDs;
if (categories == null) categories = DefaultCategories;
var methodName = logMethod?.Method?.Name?.Trim();
if (string.IsNullOrEmpty(methodName)) methodName = "Default (Console.WriteLine)";
if (DbContextExtensionLogging.DoVerbose)
{
Console.WriteLine("FLEXLOGGER EFCore " + VERSION.ToString() + " (C) Dr. Holger Schwichtenberg 2016-2017 " + methodName);
Console.WriteLine("FLEXLOGGER Start Logging to " + methodName);
Console.WriteLine("FLEXLOGGER Event-IDs: " + String.Join(";", eventsIDs));
Console.WriteLine("FLEXLOGGER Categories: " + String.Join(";", categories));
}
// Make sure we only get one LoggerFactory for each LogMethod!
var id = ctx.GetType().FullName + "_" + methodName.Replace(" ", "");
if (!loggerFactories.ContainsKey(id))
{
if (verbose) Console.WriteLine("New Logger Provider!");
var lp = new FlexLoggerProvider(logMethod, categories, eventsIDs);
loggerFactories.Add(id, lp);
// Get ServiceProvider
var serviceProvider = ctx.GetInfrastructure();
// Get Logger-Factory
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
// Add Provider to Factory
loggerFactory.AddProvider(lp);
}
}
}
/// <summary>
/// LoggerProvider for FlexLogger (C) Dr. Holger Schwichtenberg www.IT-Visions.de
/// </summary>
public class FlexLoggerProvider : ILoggerProvider
{
public Action<string> _logMethod;
public List<int> _eventIDs = null;
public List<string> _categories = null;
public FlexLoggerProvider(Action<string> logMethod = null, List<string> categories = null, List<int> eventIDs = null)
{
_logMethod = logMethod;
_eventIDs = eventIDs;
_categories = categories;
if (_eventIDs == null) _eventIDs = new List<int>();
if (_categories == null) _categories = new List<string>();
}
/// <summary>
/// Constructor is called for each category. Here you have to specify which logger should apply to each category
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public ILogger CreateLogger(string categoryName)
{
if (_categories == null || _categories.Count == 0 || _categories.Contains(categoryName))
{
if (DbContextExtensionLogging.DoVerbose) Console.WriteLine("FLEXLOGGER CreateLogger: " + categoryName + ": Yes");
return new FlexLogger(this._logMethod, this._eventIDs);
}
if (DbContextExtensionLogging.DoVerbose) Console.WriteLine("FLEXLOGGER CreateLogger: " + categoryName + ": No");
return new NullLogger(); // return NULL nicht erlaubt :-(
}
public void Dispose()
{ }
/// <summary>
/// Log output to console or custom method
/// </summary>
private class FlexLogger : ILogger
{
private static int count = 0;
readonly Action<string> logMethod;
readonly List<int> _EventIDs = null;
public FlexLogger(Action<string> logMethod, List<int> eventIDs)
{
count++;
this._EventIDs = eventIDs;
if (logMethod is null) this.logMethod = ConsoleWriteLineColor;
else this.logMethod = logMethod;
}
private static void ConsoleWriteLineColor(object s)
{
var farbeVorher = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(s);
Console.ForegroundColor = farbeVorher;
}
public bool IsEnabled(LogLevel logLevel) => true;
private static long Count = 0;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!DbContextExtensionLogging.DoLogging) return;
if (Assembly.GetAssembly(typeof(Microsoft.EntityFrameworkCore.DbContext)).GetName().Version.Major == 1 || (this._EventIDs != null && (this._EventIDs.Contains(eventId.Id) || this._EventIDs.Count == 0)))
{
Count++;
string text = $"{Count:000}:{logLevel} #{eventId.Id} {eventId.Name}:{formatter(state, exception)}";
// Call log method now
logMethod(text);
}
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
}
/// <summary>
/// No Logging
/// </summary>
private class NullLogger : ILogger
{
public bool IsEnabled(LogLevel logLevel) => false;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{ }
public IDisposable BeginScope<TState>(TState state) => null;
}
}
}
Listing 12-1Entity Framework Core Extensions for Easy Logging
日志类别
不幸的是,微软在 Entity Framework Core 版本 1.x 和 2.0 之间更改了日志类别的名称。
以下是 Entity Framework Core 1.x 中的日志记录类别:
Microsoft.EntityFrameworkCore.Storage.Internal.SQLServerConnection
Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy
Microsoft.EntityFrameworkCore.Internal.RelationalModelValidator
Microsoft.EntityFrameworkCore.Query.Internal.SqlServerQueryCompilationContextFactory
Microsoft.EntityFrameworkCore.Query.Translators expression.Internal.SqlServerCompositeMethodCallTranslator
Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory
Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler
Microsoft.EntityFrameworkCore.DbContext
以下是实体框架核心 2.0 中的日志记录类别:
Microsoft.EntityFrameworkCore.Infrastructure
Microsoft.EntityFrameworkCore.Update
Microsoft.EntityFrameworkCore.Database.Transaction
Microsoft.EntityFrameworkCore.Database.Connection
Microsoft.EntityFrameworkCore.Model.Validation
Microsoft.EntityFrameworkCore.Query
Microsoft.EntityFrameworkCore.Database.Command
Log()
扩展方法的实现考虑了这种变化;它还考虑了类别Microsoft.EntityFrameworkCore.Query
,该类别有两个事件:Executing
(事件 ID 20100)和Executed
(事件 ID 20101)。Log()
在标准系统中仅输出事件 ID 20100。但是类别和事件 id 可以通过Log()
的参数来控制。
十三、异步编程
。NET 从 1980 年开始支持简化的异步的、基于任务的编程,使用了async
和await
。NET 框架 4.5。经典的 ADO.NET 实体框架从版本 6.0 开始就支持相关的异步操作。在 Entity Framework Core 中,从 1.0 版本开始,您就可以使用异步操作了。
异步扩展方法
实体框架核心允许使用异步设计模式读写数据。为此,Microsoft 扩展了结果集,在 Microsoft 中提供了 LINQ 转换和聚合运算符。具有异步变量的 EntityFrameworkCore . EntityFrameworkQueryableExtensions 类。现在你可以在这里找到例子,EntityFrameworkQueryableExtensions
类包括扩展方法ToListAsync()
和ToArrayAsync()
;方法SingleAsync()
、FirstAsync()
、SingleOrDefaultAsync()
、FirstOrDefaultAsync()
;以及CountAsync()
、AllAsync()
、AnyAsync()
、AverageAsync()
、MinAsync()
、MaxAsync()
、SumAsyn()
等聚合函数。SaveChangesAsync()
方法用于保存。在代码中放一行using Microsoft.EntityFrameworkCore
是使用这些扩展方法的先决条件。
ToListAsync()
清单 13-1 展示了如何使用ToListAsync()
。具有相同结果集的同步调用的唯一区别在于以下两行:
- 子程序
DataReadingAsync()
被声明为async
。 - 不叫
query.ToList()
,现在叫await query.ToListAsync()
。
图 13-1 显示实体框架核心的数据库查询和对象物化实际上与这些边缘变化是异步的。这表明在发出第一个Flight
之前,主程序已经在等待输入。输出Start Database Query
仍然在线程 10 中。然后,它返回到主程序,而数据库查询和过程的剩余部分Read DataReadingAsync()
在线程 13 中继续。
图 13-1
Output of Listing 13-1
public static async void ReadDataAsync()
{
CUI.MainHeadline("Start " + nameof(ReadDataAsync)); using (var ctx = new WWWingsContext())
{
// Define query
var query = (from f in ctx.FlightSet.Include(p => p.BookingSet).ThenInclude(b => b.Passenger) where f.Departure == "Rome" && f.FreeSeats > 0 select f).Take(1);
// Execute Query asynchronously
CUI.PrintWithThreadID("Start database query");
var flightSet = await query.ToListAsync();
CUI.PrintWithThreadID("End database query");
// Print results
foreach (Flight flight in flightSet)
{
CUI.PrintWithThreadID("Flight: " + flight.FlightNo + " from " + flight.Departure + " to " + flight.Destination + " has " + flight.FreeSeats + " free seats");
foreach (var p in flight.BookingSet.Take(5))
{
CUI.PrintWithThreadID(" Passenger: " + p.Passenger.GivenName + " " + p.Passenger.Surname);
}
}
CUI.MainHeadline("End " + nameof(ReadDataAsync));
}
}
Listing 13-1Using ToListAsync( )
SaveChangesAsync()
还有一个异步保存操作。清单 13-2 显示线程如何切换到ToList()
和SaveChangesAsync()
,图 13-2 显示输出。
图 13-2
Output of Listing 13-2
public static async void ChangeDataAsync()
{
CUI.MainHeadline("Start " + nameof(ChangeDataAsync));
using (var ctx = new WWWingsContext())
{
// Define query
var query = (from f in ctx.FlightSet.Include(p => p.BookingSet).ThenInclude(b => b.Passenger) where f.Departure == "Rome" && f.FreeSeats > 0 select f).Take(1);
// Query aynchron ausführen
CUI.PrintWithThreadID("Start database query");
var flightSet = await query.ToListAsync();
CUI.PrintWithThreadID("End database query");
// Print results
foreach (Flight flight in flightSet)
{
CUI.PrintWithThreadID("Flight: " + flight.FlightNo + " from " + flight.Departure + " to " + flight.Destination + " has " + flight.FreeSeats + " free seats”);
foreach (var b in flight.BookingSet.Take(5))
{
CUI.PrintWithThreadID(" Passenger: " + b.Passenger.GivenName + " " + b.Passenger.Surname);
CUI.PrintWithThreadID(" Start saving");
b.Passenger.Status = "A";
var count = await ctx.SaveChangesAsync();
CUI.PrintWithThreadID($" {count} Changes saved!");
}
}
CUI.Headline("End " + nameof(ChangeDataAsync));
}
}
Listing 13-2Using SaveChangesAsync()
ForEachAsync()
使用 Entity Framework,在使用转换操作符(如ToList()
)进行迭代之前,没有必要显式具体化查询。一个带有IQueryable
接口的对象上的foreach
循环足以触发数据库查询。然而,在这种情况下,当循环运行时,数据库连接保持打开,记录由IQueryable
接口的迭代器单独获取。
您还可以使用方法ForEachAsync()
异步执行这个构造,该方法逐步检索结果集,并对结果集的所有元素执行方法的方法体(清单 13-3 中 lambda 表达式形式的匿名方法)。
正如在第十章中提到的,你必须使用SaveChangesAsync()
而不是SaveChanges()
来避免仍在运行的读操作和写操作之间的事务问题。
Note
如果您想使用SaveChangesAsync(): var count = await ctx.SaveChangesAsync()
的结果,您将得到以下运行时错误:“不允许新事务,因为会话中有其他线程正在运行。”最好的解决方案是不使用ForEachAsync()
,而是使用ToListAsync()
!
public static async void AsyncForeach()
{
CUI.MainHeadline("Start " + nameof(AsyncForeach));
WWWingsContext ctx = new WWWingsContext();
// Define query
var query = (from f in ctx.FlightSet.Include(p => p.BookingSet).ThenInclude(b => b.Passenger) where f.Departure == "Rome" && f.FreeSeats > 0 select f).Take(1);
// Executing and iterate query with ForEachAsync
CUI.PrintWithThreadID("Print objects");
await query.ForEachAsync(async flight =>
{
// Print results
CUI.PrintWithThreadID("Flight: " + flight.FlightNo + " from " + flight.Departure + " to " + flight.Destination + " has " + flight.FreeSeats + " free Seats");
foreach (var p in flight.BookingSet)
{
CUI.PrintWithThreadID(" Passenger: " + p.Passenger.GivenName + " " + p.Passenger.Surname);
}
// Save changes to each flight object within the loop
CUI.PrintWithThreadID(" Start saving");
flight.FreeSeats--;
await ctx.SaveChangesAsync();
//not possible: var count = await ctx.SaveChangesAsync(); --> "New transaction is not allowed because there are other threads running in the session."
CUI.PrintWithThreadID(" Changes saved!");
});
CUI.Headline("End " + nameof(AsyncForeach));
}
Listing 13-3Using ForEachAsync( )
十四、动态 LINQ 查询
语言集成查询(LINQ)的好处之一是编译器可以在设计时验证指令。但是,在一些真实的情况下,执行命令在设计时并没有完全建立,例如,因为用户从屏幕上的许多下拉过滤器中进行选择。本章展示了结合实体框架核心部分或全部动态使用 LINQ 命令的方法。
逐步创建 LINQ 查询
在执行转换运算符之前,不会执行 LINQ 查询(称为延迟执行)。以前,您可以使用更多条件来扩展 LINQ 查询,还可以使用排序和分页。清单 14-1 显示了如何偶尔添加出发或目的地的条件,偶尔添加对有空闲座位的航班的过滤,偶尔添加排序。案例由开头的变量控制,在这里表示用户输入。
Tip
只要不对查询应用转换操作符,例如ToList()
、To Dictionary()
、ToArray()
或First()
/ Single()
,或者对枚举器或单个元素进行访问(例如,使用ElementAt()
,查询就会停留在 RAM 中,并且仍然可以被修改。
public static void LINQComposition()
{
CUI.MainHeadline(nameof(LINQComposition));
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.Log();
string departure = "Paris";
string destination = "";
bool onlyWithFreeSeats = true;
bool sortieren = true;
// Base query
IQueryable<Flight> flightQuery = (from f in ctx.FlightSet select f);
// Adding optional condition
if (!String.IsNullOrEmpty(departure)) flightQuery = from f in flightQuery where f.Departure == departure select f;
if (!String.IsNullOrEmpty(destination)) flightQuery = from f in flightQuery where f.Destination == destination select f;
// Adding optional condition using a method
if (onlyWithFreeSeats) flightQuery = FreeSeatsMustBeGreaterZero(flightQuery);
// Optional sorting
if (sortieren) flightQuery = flightQuery.OrderBy(f => f.Date);
// Send to the database now!
List<Flight> flightSet = flightQuery.ToList();
// Print the result set
Console.WriteLine("Flights found:");
foreach (var f in flightSet)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination}: {f.FreeSeats} free seats! Pilot: {f.PilotId} ");
}
}
}
static public IQueryable<Flight> FreeSeatsMustBeGreaterZero(IQueryable<Flight> query)
{
return query.Where(f => f.FreeSeats > 0);
}
Listing 14-1Composing LINQ Commands
下面的 SQL 命令是从这个程序代码中生成的,在这个例子中,有一个关于Departure
和FreeSeats
的过滤器,并按航班日期排序:
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] = @__departure_0) AND ([f].[FreeSeats] > 0)
ORDER BY [f].[FlightDate]
表达式树
表达式树(。NET 命名空间System.Linq.Expressions
)是所有 LINQ 查询的基础。实体框架核心将每个 LINQ 查询转换成一个表达式树。你也可以直接使用表达式树,但是这通常非常耗时。
清单 14-2 中的 C# 程序代码首先定义了一个带有条件的 LINQ 查询。然后,根据两个局部变量的值,通过两个附加条件扩展查询。这个程序代码还利用了这样一个事实,即 LINQ 命令直到实际需要结果集时才执行(也称为延迟执行)。
public static void ExpressionTreeTwoConditions()
{
CUI.MainHeadline(nameof(ExpressionTreeTwoConditions));
string destination = "Rome";
short? minNumberOfFreeSeats = 10;
using (WWWingsContext ctx = new WWWingsContext())
{
// Base query
IQueryable<BO.Flight> query = from flight in ctx.FlightSet where flight.FlightNo < 300 select flight;
// Optional conditions
if (!String.IsNullOrEmpty(destination) && minNumberOfFreeSeats > 0)
{
// Define query variable
ParameterExpression f = Expression.Parameter(typeof(BO.Flight), "f");
// Add first condition
Expression left = Expression.Property(f, "Destination");
Expression right = Expression.Constant(destination);
Expression condition1 = Expression.Equal(left, right);
// Add second condition
left = Expression.Property(f, "FreeSeats");
right = Expression.Constant((short?)minNumberOfFreeSeats, typeof(short?));
Expression condition2 = Expression.GreaterThan(left, right);
// Connect conditions with AND operator
Expression predicateBody = Expression.And(condition1, condition2);
// Build expression tree
MethodCallExpression whereCallExpression = Expression.Call(
typeof(Queryable),
"Where",
new Type[] { query.ElementType },
query.Expression,
Expression.Lambda<Func<BO.Flight, bool>>(predicateBody, new ParameterExpression[] { f }));
// Create query from expression tree
query = query.Provider.CreateQuery<BO.Flight>(whereCallExpression);
}
ctx.Log();
// Print the result set
Console.WriteLine("Flights found:");
foreach (BO.Flight f in query.ToList())
{
Console.WriteLine($"Flight No {f.FlightNo} from {f.Departure} to {f.Destination}: {f.FreeSeats} free seats! Pilot: {f.PilotId} ");
}
}
}
Listing 14-2Extending LINQ Commands with Expression Trees
该清单生成以下 SQL 命令:
SELECT [flight].[FlightNo], [flight].[AircraftTypeID], [flight].[AirlineCode], [flight].[CopilotId], [flight].[FlightDate], [flight].[Departure], [flight].[Destination], [flight].[FreeSeats], [flight].[LastChange], [flight].[Memo], [flight].[NonSmokingFlight], [flight].[PilotId], [flight].[Price], [flight].[Seats], [flight].[Strikebound], [flight].[Timestamp], [flight].[Utilization]
FROM [Flight] AS [flight]
WHERE ([flight].[FlightNo] < 300) AND ((CASE
WHEN [flight].[Destination] = N'Rome'
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END & CASE
WHEN [flight].[FreeSeats] > 10
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1)
当然,这也适用于许多在运行时不固定的条件,如清单 14-3 所示,其中条件作为SortedDictionary
传递。
public static void ExpressionTreeNumerousConditions()
{
CUI.MainHeadline(nameof(ExpressionTreeNumerousConditions));
// Input data
var filters = new SortedDictionary<string, object>() { { "Departure", "Berlin" }, { "Destination", "Rome" }, { "PilotID", 57 } };
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.Log();
// Base query
var baseQuery = from flight in ctx.FlightSet where flight.FlightNo < 1000 select flight;
ParameterExpression param = Expression.Parameter(typeof(BO.Flight), "f");
Expression completeCondition = null;
foreach (var filter in filters)
{
// Define condition
Expression left = Expression.Property(param, filter.Key);
Expression right = Expression.Constant(filter.Value);
Expression condition = Expression.Equal(left, right);
// Add to existing conditions using AND operator
if (completeCondition == null) completeCondition = condition;
else completeCondition = Expression.And(completeCondition, condition);
}
// Create query from expression tree
MethodCallExpression whereCallExpression = Expression.Call(
typeof(Queryable),
"Where",
new Type[] { baseQuery.ElementType },
baseQuery.Expression,
Expression.Lambda<Func<BO.Flight, bool>>(completeCondition, new ParameterExpression[] { param }));
// Create query from expression tree
var Q_Endgueltig = baseQuery.Provider.CreateQuery<BO.Flight>(whereCallExpression);
// Print the result set
Console.WriteLine("Flights found:");
foreach (var f in Q_Endgueltig)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination}: {f.FreeSeats} free seats! Pilot: {f.PilotId} ");
}
}
}
Listing 14-3Extending LINQ Commands with Expression Trees
清单 14-3 产生以下 SQL 命令:
SELECT [flight].[FlightNo], [flight].[AircraftTypeID], [flight].[AirlineCode], [flight].[CopilotId], [flight].[FlightDate], [flight].[Departure], [flight].[Destination], [flight].[FreeSeats], [flight].[LastChange], [flight].[Memo], [flight].[NonSmokingFlight], [flight].[PilotId], [flight].[Price], [flight].[Seats], [flight].[Strikebound], [flight].[Timestamp], [flight].[Utilization]
FROM [Flight] AS [flight]
WHERE ([flight].[FlightNo] < 1000) AND (((CASE
WHEN [flight].[Departure] = N'Berlin'
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END & CASE
WHEN [flight].[Destination] = N'Rome'
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) & CASE
WHEN [flight].[PilotId] = 57
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END) = 1)
使用动态 LINQ
如果一步一步地创建 LINQ 查询是不够的,您不一定要使用表达式树。另一种方法是使用库动态 LINQ。动态 LINQ 不属于。NET 框架,不是一个正式的附加组件。动态 LINQ 只是微软在一个样本集合中使用的一个例子(参见 http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx
)。然而,这个例子被 Scott Guthrie ( http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx
)的一篇博客文章“拔高”,从此被广泛使用。最初的例子在根名称空间System
中有System.Linq.Dynamic
,这是不寻常的,建议微软可以将它集成到。NET 框架的未来。这种情况尚未发生,似乎也不会再出现在议事日程上。动态 LINQ 由几个类组成,大约有 2000 行代码。最重要的课是DynamicQueryable
。这个类为IQueryable
接口提供了很多扩展方法,比如Where()
、OrderBy()
、GroupBy()
和Select()
,这些方法都接受字符串。
清单 14-4 展示了一个比表达式树解决方案更优雅的动态 LINQ 解决方案。
Note
不幸的是,在动态 LINQ 中没有动态连接。,但是你可以在网上找到解决方案( http://stackoverflow.com/questions/389094/how-to-create-a-dynamic-linq-join-extension-method
)。
public static void DynamicLINQNumerousCondition()
{
CUI.MainHeadline(nameof(DynamicLINQNumerousCondition));
// input data
var filters = new SortedDictionary<string, object>() { { "Departure", "Berlin" }, { "Destination", "Rome" }, { "PilotID", 57 } };
string sorting = "FreeSeats desc";
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.Log();
// base query
IQueryable<BO.Flight> query = from flight in ctx.FlightSet where flight.FlightNo < 1000 select flight;
// Add conditions
foreach (var filter in filters)
{
Console.WriteLine(filter.Value.GetType().Name);
switch (filter.Value.GetType().Name)
{
case "String":
query = query.Where(filter.Key + " = \"" + filter.Value + "\""); break;
default:
query = query.Where(filter.Key + " = " + filter.Value); break;
}
}
// optional sorting
if (!String.IsNullOrEmpty(sorting)) query = query.OrderBy(sorting);
// Print the result set
Console.WriteLine("Flights found:");
foreach (var f in query)
{
Console.WriteLine($"Flight Nr {f.FlightNo} from {f.Departure} to {f.Destination}: {f.FreeSeats} free seats!");
}
}
}
Listing 14-4Use of Dynamic LINQ
以下 SQL 命令由该程序代码产生:
SELECT [flight].[FlightNo], [flight].[AircraftTypeID], [flight].[AirlineCode], [flight].[CopilotId], [flight].[FlightDate], [flight].[Departure], [flight].[Destination], [flight].[FreeSeats], [flight].[LastChange], [flight].[Memo], [flight].[NonSmokingFlight], [flight].[PilotId], [flight].[Price], [flight].[Seats], [flight].[Strikebound], [flight].[Timestamp], [flight].[Utilization]
FROM [Flight] AS [flight]
WHERE ((([flight].[FlightNo] < 1000) AND ([flight].[Departure] = N'Berlin')) AND ([flight].[Destination] = N'Rome')) AND ([flight].[PilotId] = 57)
ORDER BY [flight].[FreeSeats] DESC
十五、使用 SQL、存储过程和表值函数读取和修改数据
如果 LINQ 和实体框架核心 API 在功能或性能方面不够,您可以将任何 SQL 命令直接发送到数据库,包括调用存储过程和使用表值函数(tvf)。
LINQ 和实体框架核心 API ( Add()
、Remove()
、SaveChanges()
等等)都是 SQL 的抽象。实体框架核心(或相应的数据库提供者)将 LINQ 和 API 调用转换为 SQL。在许多情况下,Entity Framework Core 提供的抽象非常适合向数据库发送高效、健壮的数据库管理系统中立的命令。但是 LINQ 和 API 不能做 SQL 能做的所有事情,也不是实体框架核心发送给数据库的所有东西都足够强大。
即使在经典的实体框架中,您也能够向数据库发送 SQL 命令,而不是使用 LINQ。在实体框架核心中,你有一些同样的能力,但是它们采取了不同的形式。在某些情况下,有更多的选项,但在其他情况下,选项比经典实体框架中的少。
使用 FromSql()编写查询
对于返回实体框架核心上下文已知的实体类型的 SQL 查询,FromSql()
和FromSql<EntityType>()
方法在DbSet<EntityType>
类中可用。结果是一个IQueryable<EntityType>
(见清单 15-1 )。
Note
您不应该将 SQL 命令组成一个字符串,因为这样会带来 SQL 注入攻击的风险。
public static void Demo_SQLDirect1()
{
CUI.MainHeadline(nameof(Demo_SQLDirect1));
string departure = "Berlin";
using (var ctx = new WWWingsContext())
{
ctx.Log();
IQueryable<Flight> flightSet = ctx.FlightSet.FromSql("Select * from Flight where Departure='" + departure + "'");
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-1SQL Query in Entity Framework Core, Risking a SQL Injection Attack
最好使用。NET 占位符{0}
、{1}
、{2}
等等(参见清单 15-2 )。这些占位符由实体框架核心作为参数化的 SQL 命令来处理,因此 SQL 注入攻击是不可能的。
public static void Demo_SQLDirect2()
{
CUI.MainHeadline(nameof(Demo_SQLDirect2));
string departure = "Berlin";
using (var ctx = new WWWingsContext())
{
ctx.Log();
IQueryable<Flight> flightSet = ctx.FlightSet.FromSql("Select * from Flight where Departure={0}", departure);
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-2SQL Query in Entity Framework Core Without the Risk of SQL Injection Attack
从 Entity Framework Core 2.0 开始,甚至可以使用字符串插值,这种方法从 C# 6.0 就已经存在了(见清单 15-3 )。
public static void Demo_SQLDirect3()
{
CUI.MainHeadline(nameof(Demo_SQLDirect3));
string departure = "Berlin";
string destination = "Rome";
using (var ctx = new WWWingsContext())
{
ctx.Log();
IQueryable<Flight> flightSet = ctx.FlightSet.FromSql($@"Select * from Flight where Departure={departure} and Destination={destination}");
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-3Third Variant of SQL Query in Entity Framework
在清单 15-2 和清单 15-3 中,您不必将占位符放在单引号中。严格地说,这里不应该使用单引号,因为 Entity Framework Core 将查询转换为参数化查询(参数变成了类dbParameter
的实例)。在这两个清单中,数据库接收一个带参数的 SQL 命令,如下所示:
Select * from Flight where departure = @p0
或者使用两个参数,如下所示:
Select * from Flight where departure= @p0 and destination = @p1
因此,你可以免受 SQL 注入的攻击;您不应该将 SQL 命令编写为字符串,以避免此类漏洞!
与传统的实体框架一样,SQL 命令必须提供所有的列来完全填充实体对象。尚不支持部分填充(投影)。任何部分填充的尝试都会失败,因为实体框架核心在运行时会报错,并显示以下错误消息:“FromSql 的结果中不存在所需的列 xy。”直到实体对象的所有属性在结果集中都有对应的属性时,才会发生这种情况。
一起使用 LINQ 和 SQL
经典实体框架中的实现被称为DbSet<EntityClass>.SqlQuery()
,并且没有返回一个IQueryable<EntityClass>
作为返回对象;相反,它返回了一个DbRawSqlQuery<EntityType>
的实例。返回IQueryable<EntityClass>
的好处是现在可以在一个查询中混合使用 SQL 和 LINQ。
这里有一个例子:
IQueryable<Flight> Flightlist = ctx.FlightSet.FromSql("Select * from Flight where departure = {0}", location);
Console.WriteLine (Flight list.Count());
foreach(var Flight in Flight list) {...}
这里实体框架核心将把flightSet.Count()
转换成如下:
SELECT COUNT(*)
FROM (
Select * from Flight where departure = 'Berlin'
) AS [f]
因此,自行编写的 SQL 命令作为子查询嵌入到由 LINQ 操作生成的查询中。
在这种情况下,有三次到数据库的往返,两次用于计数,一次用于收集记录。与 LINQ 一样,在使用FromSql()
时,您应该确保使用一个转换操作符,比如ToList()
。在清单 15-4 中,结果集现在将被直接获取,而不是计数三次,您只有一次对 RAM 中物化对象集长度的查询,这要快得多!
public static void Demo_SQLDirect4()
{
CUI.MainHeadline(nameof(Demo_SQLDirect4));
string departure = "Berlin";
using (var ctx = new WWWingsContext())
{
ctx.Log();
List<Flight> flightSet = ctx.FlightSet.FromSql($@"Select * from Flight where Departure={departure}").ToList();
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-4ToList() Ensures That There Is Only One Round-Trip to the Database
更令人印象深刻的是,清单 15-5 展示了在实体框架核心中将 SQL 和 LINQ 结合在一起的可能性,其中Include()
甚至加载链接的记录。用FromSql()
是不可能的。
public static void Demo_SQLDirectAndLINQComposition()
{
CUI.MainHeadline(nameof(Demo_SQLDirectAndLINQComposition));
string departure = "Berlin";
using (var ctx = new WWWingsContext())
{
ctx.Log();
var flightSet = ctx.FlightSet.FromSql("Select * from Flight where Departure={0}", departure).Include(f => f.Pilot).Where(x => x.FreeSeats > 10).OrderBy(x => x.FreeSeats).ToList();
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-5Compiling SQL and LINQ in Entity Framework Core
使用清单 15-5 中的程序代码,数据库接收以下 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], [x.Pilot].[PersonID], [x.Pilot].[Birthday], [x.Pilot].[DetailID], [x.Pilot].[Discriminator], [x.Pilot].[EMail], [x.Pilot].[GivenName], [x.Pilot].[PassportNumber], [x.Pilot].[Salary], [x.Pilot].[SupervisorPersonID], [x.Pilot].[Surname], [x.Pilot].[FlightHours], [x.Pilot].[FlightSchool], [x.Pilot].[LicenseDate], [x.Pilot].[PilotLicenseType]
FROM (
Select * from Flight where Departure=@p0
) AS [x]
INNER JOIN [Employee] AS [x.Pilot] ON [x].[PilotId] = [x.Pilot].[PersonID]
WHERE ([x.Pilot].[Discriminator] = N'Pilot') AND ([x].[FreeSeats] > 10)
ORDER BY [x].[FreeSeats]
使用存储过程和表值函数
用FromSql()
你也可以调用存储过程,它提供一个结果集,你可以优雅地调用表值函数(清单 15-6 )。但是,应该注意,可组合性只适用于表值函数。
/// <summary>
/// Use of a stored procedure that delivers Flight records
/// </summary>
public static void Demo_SP()
{
CUI.MainHeadline(nameof(Demo_SP));
using (var ctx = new WWWingsContext())
{
ctx.Log();
var flightSet = ctx.FlightSet.FromSql("EXEC GetFlightsFromSP {0}", "Berlin").Where(x => x.FreeSeats > 0).ToList();
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-6Using a Stored Procedure That Delivers Flight Records
该清单在数据库中运行以下语句:
EXEC GetFlightsFromSP @p0
补充条件(此处为FreeSeats
)在 RAM 中执行。
然而,在清单 15-7 中,附加条件是在数据库管理系统中执行的,因为这里调用了一个表值函数。
public static void Demo_TVF()
{
CUI.MainHeadline(nameof(Demo_TVF));
using (var ctx = new WWWingsContext())
{
ctx.Log();
var flightSet = ctx.FlightSet.FromSql("Select * from GetFlightsFromTVF({0})", "Berlin").Where(x => x.FreeSeats > 10).ToList();
Console.WriteLine(flightSet.Count());
foreach (var flight in flightSet)
{
Console.WriteLine(flight);
}
Console.WriteLine(flightSet.Count());
}
}
Listing 15-7Using a Table-Valued Function That Delivers Flight Records
清单 15-7 在数据库中运行以下内容:
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 (
Select * from GetFlightsFromTVF(@p0)
) AS [x]
WHERE [x].[FreeSeats] > 10
然而,在 Entity Framework Core 中,既没有用于存储过程和表值函数的包装方法的程序代码生成器(在经典 Entity Framework 中的 Database First 中可用),也没有用于INSERT
、UPDATE
和DELETE
的存储过程的 SQL 生成器(在经典 Entity Framework 中的 Code First 中可用)。
Tip
第三方工具实体开发者(参见第二十章)为存储过程和表值函数的包装方法提供了程序代码生成器。
使用不存在的类作为结果集
不幸的是,与它的前身相比,Entity Framework Core 也有(至少到目前为止)一个很大的限制。在经典的实体框架中,SqlQuery()
不仅在类DbSet<EntityType>
的实例中提供,也在上下文类的Database
对象中提供。您还可以指定不是实体类型的其他类型,换句话说,其他自定义的类或者甚至是基本数据类型,在这些类型中,经典的实体框架将具体化查询结果。
遗憾的是,实体框架核心还不能做到这一点( https://github.com/aspnet/EntityFramework/issues/1862
)。任何将除了FromSql()
之外的对象类型指定为实体类型的尝试都不会编译。
var flightSet = ctx.FromSql<FligthDTO>("Select FlightNo, Departure, Destination, Date from Flight");
用下面的方法做一个实验Set <T>()
:
flightSet = ctx.Set<FligthDTO>().FromSql("Select FlightNo, Departure, Destination, Date from Flight");
编译,但实体框架核心随后在运行时表示不支持此操作:“无法为' FlightDTO '创建 dbSet,因为此类型未包含在上下文的模型中。”
Preview
微软将在实体框架核心 2.1 版中引入到任意类型的映射;参见附录 C 。
清单 15-8 显示了对Database
对象中的ExecuteSqlQuery()
方法的改进,它只返回一个DbDataReader
对象,并且不允许物化。这个扩展方法将在清单 15-9 中使用。
public static class RDFacadeExtensions
{
public static RelationalDataReader ExecuteSqlQuery(this DatabaseFacade databaseFacade, string sql, params object[] parameters)
{
var concurrencyDetector = databaseFacade.GetService<IConcurrencyDetector>();
using (concurrencyDetector.EnterCriticalSection())
{
var rawSqlCommand = databaseFacade
.GetService<IRawSqlCommandBuilder>()
.Build(sql, parameters);
return rawSqlCommand
.RelationalCommand
.ExecuteReader(
databaseFacade.GetService<IRelationalConnection>(),
parameterValues: rawSqlCommand.ParameterValues);
}
}
}
Listing 15-8Database Extension Method.ExecuteSqlQuery()
public static void Demo_Datareader()
{
CUI.MainHeadline(nameof(Demo_Datareader));
string Ort = "Berlin";
using (var ctx = new WWWingsContext())
{
RelationalDataReader rdr = ctx.Database.ExecuteSqlQuery("Select * from Flight where Departure={0}", Ort);
DbDataReader dr = rdr.DbDataReader;
while (dr.Read())
{
Console.WriteLine("{0}\t{1}\t{2}\t{3} \n", dr[0], dr[1], dr[2], dr[3]);
}
dr.Dispose();
}
}
Listing 15-9Using Database.ExecuteSqlQuery()
使用不带结果集的 SQL DML 命令
不返回结果集的 SQL 数据操作语言(DML)命令,如INSERT
、UPDATE
和DELETE
,可以在实体框架核心中执行,就像在经典实体框架中一样,在Database
对象中有ExecuteSqlCommand()
。您将获得受影响记录的数量(参见清单 15-10 )。
public static void Demo_SqlCommand()
{
CUI.MainHeadline(nameof(Demo_SqlCommand));
using (var ctx = new WWWingsContext())
{
var count = ctx.Database.ExecuteSqlCommand("Delete from Flight where flightNo > {0}", 10000);
Console.WriteLine("Number of deleted records: " + count);
}
}
Listing 15-10Using Database.ExecuteSqlCommand()
十六、映射的提示和技巧
本章描述了影响实体类到数据库模式映射的其他方法。这些可能性中的许多没有显示在世界之翼的例子中,因为它们不适合它。
阴影属性
实体框架核心可以为数据库表中在实体类中没有对应属性或字段的那些列创建影子属性(也称为影子状态属性)。
自动阴影属性
如果导航关系的实体类中没有匹配的外键属性,则会自动创建影子属性。因为对于大多数关系,数据库模式需要一个外键列,所以该列自动成为影子属性。如果导航关系的对应方有几个主键列,则相应地创建几个外键列,每个外键列有一个 shadow 属性。
外键列和影子属性由导航属性的名称和主类的主键名组成。这样做,实体框架核心避免了单词重复(见表 16-1 )。
表 16-1
Automatic Naming for Shadow Properties
| 主类的主键 | 导航属性的名称 | 外键列/阴影属性的名称 | | :-- | :-- | :-- | | `MainClassID` | `MainClass` | `MainClassID` | | `ID` | `MainClass` | `MainClassID` | | `ID` | `Anything` | `AnythingID` | | `MainClassID` | `Anything` | `AnythingMainClassID` | | `MainClassID1`和`MainClassID2`(组合键) | `Anything` | `AnythingMainClassID1`和`AnythingMainClassID2` |定义阴影属性
您也可以使用Property()
方法手动定义阴影属性。清单 16-1 展示了如何添加一个名为LastChange
的DateTime
影子属性。但是,这个名称存储在一个变量中,因此可以更改。
Note
更改变量ShadowPropertyName
的内容必须在上下文类的第一次实例化之前完成,因为OnModelCreating()
只被调用一次,即第一次使用上下文类时。
public class WWWingsContext: DbContext
{
static public string ShadowPropertyName = "LastChange";
...
protected override void OnModelCreating (ModelBuilder builder)
{
...
builder.Entity<Flight>().Property<DateTime>(ShadowPropertyName);
}
}
Listing 16-1Defining a Shadow Property in OnModelCreating() of the Context Class
获取实体类的所有影子属性的输出
您可以从EntityEntry<T>
实例的 properties 对象集中获得一个实体类的所有属性的列表(真实属性和阴影属性),这是通过上下文类的Entry()
方法获得的(清单 16-2 )。
using (WWWingsContext ctx = new WWWingsContext())
{
var flight = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == flightNo);
foreach (var p in ctx.Entry(flight).Properties)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata.IsShadowProperty);
}
}
Listing 16-2Printing a List of All Properties of an Entity Class Including the Shadow Properties
读取和更改阴影属性
不能在实体对象上直接使用 shadow 属性,因为相应的数据库列没有 real 属性。因此,使用通过EntityEntry<T>
的实例进行,该实例通过上下文类的方法Entry()
获得。然后您可以调用方法Property("ColumnName")
并从提供的PropertyEntry
对象中查询CurrentValue
属性。
ctx.Entry(flight).Property("LastChange").CurrentValue
顺便说一下,这样您就可以从实体对象中访问任何信息,包括真实的属性。
ctx.Entry(flight).Property("FreeSeats").CurrentValue
然而,这通常是不做的,因为通过实体对象访问更容易。
flight.FreeSeats
Note
您只能通过Property ("Name")
访问真实属性和阴影属性。如果数据库表有额外的列,它们不能被实体框架访问。调用ctx.Entry(Flight).Property("abc").CurrentValue
导致以下运行时错误:“找不到实体类型‘Flight’的属性‘ABC’。请确保该属性存在,并且已包含在模型中。
您也可以通过CurrentValue
改变数值,如下所示:
ctx.Entry(Flight).Property("LastChange").CurrentValue = DateTime.Now;
清单 16-3 展示了Flight
实体类上LastChange
shadow 属性的使用。
public static void ReadAndChangeShadowProperty()
{
int flightNo = 101;
CUI.MainHeadline(nameof(ReadAndChangeShadowProperty));
using (WWWingsContext ctx = new WWWingsContext())
{
var flight = ctx.FlightSet.SingleOrDefault(x => x.FlightNo == flightNo);
CUI.Headline("List of all shadow property of type Flight");
foreach (var p in ctx.Entry(flight).Properties)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata.IsShadowProperty);
}
CUI.Print("Before: " + flight.ToString() + " / " + ctx.Entry(flight).State, ConsoleColor.Cyan);
Console.WriteLine("Free seats: " + ctx.Entry(flight).Property("FreeSeats").CurrentValue);
Console.WriteLine("Last change: " + ctx.Entry(flight).Property("LastChange").CurrentValue);
CUI.PrintWarning("Changing object...");
flight.FreeSeats += 1;
ctx.Entry(flight).Property("LastChange").CurrentValue = DateTime.Now;
CUI.Print("After: " + flight.ToString() + " / " + ctx.Entry(flight).State, ConsoleColor.Cyan);
Console.WriteLine("Free seats: " + ctx.Entry(flight).Property("FreeSeats").CurrentValue);
Console.WriteLine("Last change: " + ctx.Entry(flight).Property("LastChange").CurrentValue);
var count = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count);
}
}
Listing 16-3Using a Shadow Property
使用阴影属性编写 LINQ 查询
您还可以在语言集成查询(LINQ)中使用阴影属性。这里不能用Entry()
和Property()
;你必须使用特殊的建筑EF.Property<T>()
。类EF
是实体框架核心的静态类。
这种结构不仅在条件中允许,而且在排序和投影中也允许。清单 16-4 标识了最近两天发生的最后一次变更。
CUI.Headline("LINQ query using a Shadow Property");
using (WWWingsContext ctx = new WWWingsContext())
{
var date = ctx.FlightSet
.Where(c => EF.Property<DateTime>(c, WWWingsContext.ShadowStateProp) > DateTime.Now.AddDays(-2))
.OrderByDescending(c => EF.Property<DateTime>(c, WWWingsContext.ShadowStateProp))
.Select(x => EF.Property<DateTime>(x, WWWingsContext.ShadowStateProp))
.FirstOrDefault();
Console.WriteLine("Last change: " + date);
}
Listing 16-4LINQ Queries with Shadow Properties
实际示例:每次保存时自动更新阴影属性
如果您想对开发人员隐藏信息,阴影属性尤其有用。清单 16-5 展示了如何在上下文类中覆盖SaveChanges()
方法,每次保存对Flight
对象的更改时,该方法会自动将LastChange
shadow 属性更新为当前日期和时间。
public override int SaveChanges()
{
// Detect changes
this.ChangeTracker.DetectChanges();
// Search all new and changed flights
var entries = this.ChangeTracker.Entries<Flight>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
if (!String.IsNullOrEmpty(ShadowStateProp))
{
// set the Shadow State column "LastChange" for all of them
foreach (var entry in entries)
{
entry.Property(ShadowStateProp).CurrentValue = DateTime.Now;
}
}
// Save changes (we do not need DetectChanges() to be called again!)
this.ChangeTracker.AutoDetectChangesEnabled = false;
var result = base.SaveChanges(); // Call base class now
this.ChangeTracker.AutoDetectChangesEnabled = true;
return result;
}
Listing 16-5Using a Shadow Property in the Overwritten SaveChanges() Method
计算列
从实体框架核心的角度来看,计算列是数据库管理系统为其赋值的那些数据库列。这些可能如下:
- 自动增加值列(标识列),如
Person
类中的PersonID
以及从Person
派生的Employee
、Pilot
和Passenger
类 - 时间戳列(参见第十一章
- 具有默认值的列
- 具有计算公式的列
自动选择
在所有这些情况下,在任何生成的INSERT
或UPDATE
之后,实体框架核心用一个自动的SELECT
来响应,以将计算列的新值从数据库管理系统读入 RAM。然而,在默认列的情况下,这只有在INSERT
之后才有意义。因此,实体框架核心知道三种策略以及 Fluent API 中的相应方法:
ValueGeneratedOnAdd()
:只有在INSERT
之后才会执行SELECT
。ValueGeneratedOnAddOrUpdate()
:在INSERT
和UPDATE
之后都执行SELECT
。ValueGeneratedNever()
:在INSERT
或UPDATE
之后不执行SELECT
。
ValueGeneratedNever()
是所有列的默认值,但以下三种情况除外:
- 默认情况下,由类型为
Int16
(short
)、Int32
(int
)或Int64
(long
)的单个整数列组成的主键列被创建为带有ValueGeneratedOnAdd()
的自动递增列。 - 默认列自动得到
ValueGeneratedOnAdd()
。 - 用
[Timestamp]
标注的字节数组(byte[]
)列被自动赋予ValueGeneratedOnAddOrUpdate()
。
Note
虽然 Microsoft SQL Server 确实允许类型为tinyint
(即byte
)的标识列,但 Entity Framework Core 始终为Byte
数据类型设置主键不使用自动增量值。
实际示例:使用计算公式创建列
与其前身相比,实体框架核心支持在程序代码中定义计算公式,以便在数据库管理系统中执行正向工程。
您可以在实体类中为计算列创建一个属性。例如,实体类Flight
将获得一个名为Utilization
的属性,它将是预订座位的百分比。它还得到了Seats
(飞机座位总数)和FreeSeats
。将计算列的Utilization
属性的 setter 声明为私有是有意义的,因为该对象的任何用户都不能设置该值。这里不能完全省略 setter,因为 Entity Framework Core 需要一种方法来设置从数据库管理系统接收的值。
public class Flight
{
public int FlightNo {get; set; }
...
public short Seats {get; set; }
public short? FreeSeats {get; set; }
public decimal? Utilization {get; private set; }
}
现在用HasComputedColumnSql()
在OnModelCreating()
的 Fluent API 中定义一个公式。在这种情况下,每个Flight
的利用率百分比是根据空闲座位数和总座位数计算出来的。
modelBuilder.Entity<Flight>().Property(p => p.Utilization)
.HasComputedColumnSql("100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
Note
没有必要使用ValueGeneratedOnAddOrUpdate()
,因为HasComputedColumnSql()
暗示了这种策略。
当创建模式迁移时,您可以在MigrationBuilder
调用中再次看到该公式。
public partial class v2_FlightCostload: Migration
{
protected override void up (MigrationBuilder migrationBuilder)
{
migration builder.AddColumn<int>(
name: "Utilization",
table: "Flight",
type: "decimal",
nullable: true,
computedColumnSql: "100.0-(([FreeSeats]*1.0)/[Seats])*100.0");
}
...
}
你也可以在数据库模式中看到这个公式(图 16-1 )。
图 16-1
Calculation formula column in Microsoft SQL Server Management Studio
在计算公式中使用列
带有公式的列的属性可以像其他列一样读取,如清单 16-6 所示。
Note
在执行SaveChanges()
之前,带有计算公式列的列的属性值不会改变。在清单 16-6 中,座位数量减少后,利用率最初显示的是旧值。只有在SaveChanges()
之后才能获得新值。
public static void ComputedColumnWithFormula()
{
CUI.MainHeadline(nameof(ComputedColumnWithFormula));
int flightNo = 101;
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.Log();
var flight = ctx.FlightSet.Find(flightNo);
Console.WriteLine($"BEFORE: {flight}: Utilization={flight.Utilization:##0.00}%");
flight.FreeSeats -= 10;
//not possible: flight.Utilization = 100;
// The change is not yet visible in the Utilization, since the Utilization is calculated in the DBMS
Console.WriteLine($"After changes: {flight}: Utilization={flight.Utilization:##0.00}%");
ctx.SaveChanges();
// The change in Utilization is now visible
Console.WriteLine($"After saving: {flight}: Utilization={flight.Utilization:##0.00}%");
CUI.Headline("Metadata of Flight properties");
foreach (PropertyEntry p in ctx.Entry(flight).Properties)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata.ValueGenerated);
}
}
}
Listing 16-6The Utilization Column in the Flight Class Is Based on a Formula
图 16-2 显示了清单 16-6 的输出。请注意以下几点:
图 16-2
Output of Listing 16-6
- 利用率仅在
SaveChanges()
后更新。 - 利用率列有策略
OnAddOrUpdate
(通过Metadata
.
ValueGenerated
在PropertyEntry
对象中得到的)。 - 因此,在
SaveChanges()
之后,实体框架核心对该值(和时间戳)执行SELECT
。
在逆向工程中使用带计算公式的列
实体框架核心在使用Scaffold-DbContext
进行逆向工程时识别计算公式,并相应地创建一个属性。但是,该属性接收一个公共 setter。公式由代码生成器存储在 Fluent API 中,但这在逆向工程中没有任何意义。只有当你后来从逆向工程转向正向工程时,它才会变得有意义。
public decimal? Utilization {get; set; }
Fluent API 包含以下内容:
entity.Property(e => e.Utilization)
.HasColumnType("numeric(20, 8)")
.HasComputedColumnSql("((100.0-(([FreeSeats]*1.0)/[Seats])*100.0))")
逆向工程的基础是带有列Utilization
的数据库模式,它以前是由正向工程生成的。
默认值
实体框架核心支持由数据库管理系统设置的默认值,同时为尚未传递值的列创建记录。这种支持同时存在于正向工程和反向工程中。
定义正向工程的默认值
当没有提供显式值时,使用 Fluent API 中的方法HasDefaultValue()
和HasDefaultValueSql()
来定义由数据库管理系统分配的列默认值。默认值可以是下列值之一:
- 静态值(如数字或字符串),使用
HasDefaultValue()
- 使用
HasDefaultValueSql()
的 SQL 表达式(例如,调用类似getdate()
的函数)
以下是一些例子:
f.Property(x => x.Price).HasDefaultValue(123.45m);
f.Property(x => x.Departure).HasDefaultValue("(not set)");
f.Property(x => x.Destination).HasDefaultValue("(not set)");
f.Property(x => x.Date).HasDefaultValueSql("getdate()");
实体框架核心在创建数据库时会考虑这些默认值(参见图 16-3 )。
图 16-3
Default value for the FlightDate column in Microsoft SQL Server Management Studio
使用默认值
实体框架核心考虑定义的默认值。作为ValueOnAdd
策略的一部分,实体框架核心在INSERT
之后查询默认值,但前提是之前没有传递任何值。
清单 16-7 非常深刻地展示了这一点。
- 在代码中设置
Departure
的值。因此,实体框架核心不要求SELECT
中的出发位置。 Destination
为空。因此,实体框架核心在SELECT
中要求数据库管理系统给出的Destination
值。- 新对象中未设置
Flight Date
和Price
。因此,实体框架核心在SELECT
中查询由数据库管理系统分配的FlightDate
和Price
值。
public static void DefaultValues()
{
CUI.MainHeadline(nameof(DefaultValues));
using (WWWingsContext ctx = new WWWingsContext())
{
var pilot = ctx.PilotSet.FirstOrDefault();
ctx.Log();
var f = new Flight();
f.FlightNo = ctx.FlightSet.Max(x => x.FlightNo) + 1;
f.Departure = "Berlin";
f.Destination = null;
f.Pilot = pilot;
f.Copilot = null;
f.FreeSeats = 100;
f.Seats = 100;
CUI.Headline("Object has been created in RAM");
Console.WriteLine($"{f} Price: {f.Price:###0.00} Euro.");
ctx.FlightSet.Add(f);
CUI.Headline("Object has been connected to the ORM");
Console.WriteLine($"{f} Price: {f.Price:###0.00} Euro.");
ctx.SaveChanges();
CUI.Headline("Object has been saved");
Console.WriteLine($"{f} Price: {f.Price:###0.00} Euro.");
f.FreeSeats--;
CUI.Headline("Object has been changed in RAM");
Console.WriteLine($"{f} Price: {f.Price:###0.00} Euro.");
ctx.SaveChanges();
CUI.Headline("Object has been saved"); ;
Console.WriteLine($"{f} Price: {f.Price:###0.00} Euro.");
//if (f.Destination != "(not set)") Debugger.Break();
//if (f.Price != 123.45m) Debugger.Break();
}
}
Listing 16-7Using Default Values
图 16-4 显示默认值仅在SaveChanges()
之后设置。用UPDATE
更新后,实体框架核心不再要求默认值(只有带有Utilization
计算公式的列和数据库管理系统分配的时间戳列是UPDATE
后的SELECT
的一部分)。
图 16-4
Output of Listing 16-7
实际示例:创建对象时已经分配了默认值
如果您希望默认值在对象创建后立即在 RAM 中生效,那么您不必在数据库管理系统中分配这些默认值,而是在类的构造函数中分配(清单 16-8 )。
public class Flight
{
/// <summary>
/// Parameterless constructor
/// </summary>
public Flight()
{
// Default Values
this.Departure = "(not set)";
this.Destination = "(not set)";
this.Price = 123.45m;
this.Date = DateTime.Now;
}
...
}
Listing 16-8Assigning Default Values in the Constructor
图 16-5 使用了Date
和Price
属性的例子来显示默认值立即适用。然而,Flight
构造函数中设置的Departure
和Destination
默认值不起作用,因为程序代码覆盖了这些值。
图 16-5
Output of the Use Defaults listing when default values are specified in the constructor
使用默认值进行逆向工程
实体框架核心识别Scaffold-DbContext
上的默认值,并将它们放在 Fluent API 中。这里重要的是实体框架核心知道有一个缺省值。对于逆向工程来说,实际值是多少并不重要。事实上,您也可以稍后在这里输入一个空字符串。生成的代码总是使用HasDefaultValueSql()
,即使对于静态值也是如此。
entity.Property(e => e.FlightDate).HasDefaultValueSql("(getdate())")
entity.Property(e => e.Price).HasDefaultValueSql("((123.45))");
entity.Property(e => e.Departure)
.HasMaxLength(50)
.HasDefaultValueSql("(N'(not set)')");
entity.Property(e => e.Destination)
.HasMaxLength(50)
.HasDefaultValueSql("(N'(not set)')");
表格拆分
从 Entity Framework Core 2.0 开始,OR mapper 允许您将单个数据库表分布在多个实体类中。表格拆分如下进行:
- 为表创建一个实体类(
Master
)和一个或多个依赖类。 - 实体类实现引用依赖类的 1:1 导航属性。
- 对于这些导航属性,您在
OnModelCreating()
中调用方法OwnsOne()
。
清单 16-9 显示了一个Master
类和三个依赖的Split
类。
using ITVisions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
namespace EFC_MappingScenarios.TableSplitting
{
/// <summary>
/// In this example, several classes are deliberately implemented in one file, so that the example is clearer.
/// </summary>
class DEMO_TableSplitting
{
public static void Run()
{
CUI.MainHeadline(nameof(DEMO_TableSplitting));
using (var ctx = new MyContext())
{
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!");
}
CUI.Headline("Detail");
var obj1 = new Detail();
foreach (var p in ctx.Entry(obj1).Properties)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata.IsShadowProperty);
}
CUI.Headline("Master");
var obj2 = new Master();
foreach (var p in ctx.Entry(obj2).Properties)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata.IsShadowProperty);
}
}
}
}
class MyContext : DbContext
{
public DbSet<Master> MasterSet { get; set; }
public DbSet<Detail> DetailSet { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
// Set provider and connectring string
string connstring = @"Server=.;Database=EFC_MappingTest_TableSplitting;Trusted_Connection=True;MultipleActiveResultSets=True;";
builder.UseSqlServer(connstring);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Define a composite key
modelBuilder.Entity<Master>().HasKey(b => new { b.MasterId1, b.MasterId2 });
// Define table splitting
modelBuilder.Entity<Master>().OwnsOne(c => c.Split1);
modelBuilder.Entity<Master>().OwnsOne(c => c.Split2);
modelBuilder.Entity<Master>().OwnsOne(c => c.Split3);
}
}
public class Master
{
public int MasterId1 { get; set; }
public int MasterId2 { get; set; }
public string Memo { get; set; }
public List<Detail> DetailSet { get; set; }
public Split1 Split1 { get; set; }
public Split2 Split2 { get; set; }
public Split3 Split3 { get; set; }
}
public class Detail
{
public int DetailId { get; set; }
public string DetailMemo { get; set; }
public Master Master { get; set; }
}
public class Split1
{
public string Memo1 { get; set; }
}
public class Split2
{
public string Memo2 { get; set; }
}
public class Split3
{
public string Memo3 { get; set; }
}
}
Listing 16-9Project EFC_MappingTest, TableSplitting.cs
图 16-6 显示MasterSet
数据库表包含了Master
类以及三个名为Split1
、Split2
和Split3
的类的所有属性。
图 16-6
Created database
序列对象(序列)
序列对象(也称为序列)存在于一些数据库管理系统(例如 Oracle、Microsoft SQL Server)中,用于自动增量值的灵活分配。在 Microsoft SQL Server 中,序列自 2012 版起就存在了。
什么是序列?
序列以升序或降序返回数字,以定义的间隔生成。可以对序列进行配置,以便在达到可定义的最终值时重新启动,换句话说,形成一个循环(序列循环)。虽然标识列仅适用于tinyint
、smallint
、int
和bigint
,但是您也可以使用序列将值分配给 decimal 和 numeric 列类型。
因此,序列对象具有以下属性:
- 最小值
- 最大值
- 起始值(介于最小值和最大值之间的值),表示将传送的第一个数字
- 增量或减量(当前值为下一个值增加的值)
- 顺序循环(是或否)
与标识列不同,序列对象独立于一个表,因此可以由多个表使用。
在 Microsoft SQL Server Management Studio(SSMS)中,可以在数据库的可编程性/序列分支中看到序列对象。
序列允许您在不在表中插入行的情况下检索下一项。这是在 T-SQL 中通过以下语句完成的:
select NEXT VALUE FOR schema.NameofSequence
此后,序列号被视为已检索。如果提取是中止的事务的一部分,则不会回滚。
在正向工程中创建序列
通过HasSequence()
在 Fluent API 中定义一个序列,然后与HasDefaultValueSql()
一起使用(例如,用于主键)。清单 16-10 显示了 1000 和 1300 之间的循环序列的定义,从 1100 开始,步长为 10。然后在三个地方使用这个序列(简单主键、复合主键和其他列)。图 16-7 显示了输出。
图 16-7
Created sequence in Microsoft SQL Server Management Studio
// cyclic sequence between 1000 and 1300, step 10, starting at 1100
modelBuilder.HasSequence<int>("Setp10IDs", schema: "demo")
.StartsAt(1100).IncrementsBy(10).HasMin(1000).HasMax(1300).IsCyclic();
// Sequence used for primary key (Data type: short)
modelBuilder.Entity<EntityClass1>()
.Property(o => o.EntityClass1Id)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
// Sequence used for normal column (Data type: decimal)
modelBuilder.Entity<EntityClass2>()
.Property(o => o.Value)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
// Sequence used for part of a composite key (Data type: int)
modelBuilder.Entity<EntityClass3>().HasKey(b => new { b.EntityClass3Id1, b.EntityClass3Id2 });
modelBuilder.Entity<EntityClass3>()
.Property(o => o.EntityClass3Id1)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
Listing 16-10Creating and Applying Sequences
查看运行中的序列
清单 16-11 显示了一个独立的例子:
- 上下文类引用了三个实体类。
- 上下文类创建一个序列。
- 实体类 1 使用主键序列。
- 实体类 2 使用
Value
列的顺序(类型decimal
)。 - 实体类 3 使用序列作为主键的一部分。
- 然后程序创建并保存所有三个类的实例。
using ITVisions;
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
namespace EFC_MappingScenarios.Sequences
{
/// <summary>
/// In this example, several classes are deliberately implemented in one file, so that the example is clearer.
/// </summary>
class DEMO_SequencesDemos
{
public static void Run()
{
CUI.MainHeadline(nameof(DEMO_SequencesDemos));
using (var ctx = new Kontext())
{
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!");
}
// This will fail, because we consume more IDs that the sequence defines!
for (int i = 0; i < 30; i++)
{
var obj1 = new EntityClass1();
ctx.EntityClass1Set.Add(obj1);
CUI.Headline("EntityClass1");
Console.WriteLine($"BEFORE: PK: {obj1.EntityClass1Id}");
var count1 = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count1}.PK: {obj1.EntityClass1Id}");
CUI.Headline("EntityClass2");
var obj2 = new EntityClass2();
ctx.EntityClass2Set.Add(obj2);
Console.WriteLine($"BEFORE: PK: {obj2.EntityClass2Id} Value: {obj2.Value}");
var count2 = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count2}.PK: {obj2.EntityClass2Id} Value: {obj2.Value}");
CUI.Headline("EntityClass3");
var obj3 = new EntityClass3();
ctx.EntityClass3Set.Add(obj3);
Console.WriteLine($"BEFORE: PK: {obj3.EntityClass3Id1}/{obj3.EntityClass3Id2}");
var count3 = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count3}. PK: {obj3.EntityClass3Id1}/{obj3.EntityClass3Id2}");
}
}
}
}
class Kontext : DbContext
{
public DbSet<EntityClass1> EntityClass1Set { get; set; }
public DbSet<EntityClass2> EntityClass2Set { get; set; }
public DbSet<EntityClass3> EntityClass3Set { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
// Set provider and connectring string
string connstring = @"Server=.;Database=EFC_MappingTest_Sequences;Trusted_Connection=True;MultipleActiveResultSets=True;";
builder.UseSqlServer(connstring);
builder.EnableSensitiveDataLogging(true);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// cyclic sequence between 1000 and 1300, step 10, starting at 1100
modelBuilder.HasSequence<int>("Setp10IDs", schema: "demo")
.StartsAt(1100).IncrementsBy(10).HasMin(1000).HasMax(1300).IsCyclic();
// Sequence used for primary key (Data type: short)
modelBuilder.Entity<EntityClass1>()
.Property(o => o.EntityClass1Id)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
// Sequence used for normal column (Data type: decimal)
modelBuilder.Entity<EntityClass2>()
.Property(o => o.Value)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
// Sequence used for part of a composite key (Data type: int)
modelBuilder.Entity<EntityClass3>().HasKey(b => new { b.EntityClass3Id1, b.EntityClass3Id2 });
modelBuilder.Entity<EntityClass3>()
.Property(o => o.EntityClass3Id1)
.HasDefaultValueSql("NEXT VALUE FOR demo.Setp10IDs");
}
}
public class EntityClass1
{
public short EntityClass1Id { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
public class EntityClass2
{
public int EntityClass2Id { get; set; }
public decimal Value { get; set; }
}
public class EntityClass3
{
/// Composite PK
public int EntityClass3Id1 { get; set; }
public int EntityClass3Id2 { get; set; }
}
}
Listing 16-11Project EFC_MappingTest, Sequences.cs
图 16-8 没有显示从序列号 1100 开始的第一次运行,而是以下运行之一。对于实体类别 1,序列号 1260 被分配;对于实体类别 2,分配 1270;对于实体类别 3,分配 1280。然后,对于实体类 1,使用 1290,对于实体类 2,分配 1300。因此,到达了循环序列的值范围的末端。下一个值是 1000。
图 16-8
Output of Listing 16-11 Note
如果最终在序列循环中使用了此表中已在使用的主键的值,则会出现以下运行时错误:“违反主键约束' PK_EntityClass1Set '。无法将重复键插入对象的 dbo。EntityClass1Set。重复的键值是(…)。"
可选键
除了主键(可能由一列或多列组成)之外,表还可以有附加键(也可能由一列或多列组成),这些键唯一地标识数据库表中的每一行。为此,数据库管理系统有一些概念,如唯一索引和唯一约束。这两个概念是相似的。这是在 https://technet.microsoft.com/en-us/library/aa224827(v=sql.80).aspx
讨论的。
经典的实体框架核心只允许创建唯一的索引。在实体框架核心中,您现在还可以创建唯一的约束。实体框架核心称之为可选键。
表 16-2
Unique Index vs. Alternative Key
| 实体框架概念 | 唯一索引 | 可选键 | | :-- | :-- | :-- | | 数据库概念 | 唯一索引 | 唯一约束 | | 经典实体框架中支持 | 是的,从版本 6.1 开始 | 不 | | 实体框架核心中支持 | 是的,从版本 1.0 开始 | 是的,从版本 1.0 开始 | | 在关系中用作外键 | 不 | 是 |定义可选键
在 Fluent API 中用HasAlternateKey()
定义了一个替代键。在这里,类Detail
接收到了列Guid
的替换键。
modelBuilder.Entity<Detail>().HasAlternateKey(c => c.Guid);
与主键一样,可选键可以由多列组成。为此,使用一个匿名对象作为HasAlternateKey()
中的参数:
modelBuilder.Entity<Detail>()
.HasAlternateKey(c => new { c.Guid, Bereich = c.Area });
如果在关系定义中,您没有创建外键和主键之间的关系,而是使用父类的不同列而不是主键,则 Entity Framework Core 会自动生成替换键。
modelBuilder.Entity<Detail>()
.HasOne(p => p.Master)
.WithMany(b => b.DetailSet)
.HasForeignKey(p => p.MasterGuid)
.HasPrincipalKey(b => b.Guid);
图 16-9 显示了在 Microsoft SQL Server Management Studio 中创建的唯一约束。实体框架核心给出了以字母 AK 开头的唯一约束名称(代表“可选键”)。
除了主键之外,MasterSet
表还有一个针对Guid
列的唯一约束。除了指向MasterSet
的Guid
列的主键和外键之外,DetailSet
表还有两个惟一的约束:一个在Guid
列上,另一个在Guid
和Area
列上,作为组合键。这证明了实体框架核心允许一个列成为多个可选键的一部分。
Tip
如果可能的话,您应该总是使用关系来形成整数列,外键应该引用主键,因为这样可以提供最高的性能和最小的内存开销。图 16-9 通过GUID
列类型的列显示的关系仅用于说明目的。
图 16-9
Unique constraints in Microsoft SQL Server Management Studio Tip
如果您不喜欢实体框架核心分配的唯一约束的名称,您也可以使用HasName()
在 Fluent API 中给出自己的名称。
这里有一个例子:
// Alternative key with one column
modelBuilder.Entity<Detail>()
.HasAlternateKey(c => c.Guid).HasName("UniqueContraint_GuidOnly"); ;
// Alternative key with two columns
modelBuilder.Entity<Detail>()
.HasAlternateKey(c => new { c.Guid, Bereich = c.Area }).HasName("UniqueContraint_GuidAndArea");
查看使用中的替代键
清单 16-12 显示了一个独立的例子:
- 上下文类是指两个实体类:
Master
和Detail
。 - 除了主键之外,
Master
类还有一个用于GUID
列的替换键。 - 除了指向
MasterSet
的Guid
列的主键和外键,Detail
类还有两个备用键:一个在Guid
属性上,一个在Guid
和Area
属性上。 - 程序首先输出两个类的属性列表,并在屏幕上显示它是一个键(
IsKey()
)还是一个主键(IsPrimaryKey()
)。对于每个主键,IsKey()
也返回 true。 - 然后程序创建一个
Detail
对象和一个Master
对象。它使用GUID
连接这些对象。
图 16-10 显示了输出。
图 16-10
Output of Listing 16-12
using ITVisions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
namespace EFC_MappingScenarios.AlternateKeys
{
/// <summary>
/// In this example, several classes are deliberately implemented in one file, so that the example is clearer.
/// </summary>
class DEMO_AlternateKeys
{
public static void Run()
{
CUI.MainHeadline(nameof(DEMO_AlternateKeys));
using (var ctx = new MyContext())
{
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!");
}
CUI.MainHeadline("Metadata");
CUI.Headline("Detail");
var obj1 = new Detail();
foreach (var p in ctx.Entry(obj1).Properties)
{
Console.WriteLine(p.Metadata.Name + ": Key=" + p.Metadata.IsKey() + " PrimaryKey=" + p.Metadata.IsPrimaryKey());
}
CUI.Headline("Master");
var obj2 = new Master();
foreach (var p in ctx.Entry(obj2).Properties)
{
Console.WriteLine(p.Metadata.Name + ": Key=" + p.Metadata.IsKey() + " PrimaryKey=" + p.Metadata.IsPrimaryKey());
}
CUI.MainHeadline("Two new objects...");
var h = new Master();
h.Guid = Guid.NewGuid().ToString();
var d = new Detail();
d.Guid = Guid.NewGuid().ToString();
d.Area = "AB";
h.DetailSet.Add(d);
ctx.MasterSet.Add(h);
var count = ctx.SaveChanges();
if (count > 0)
{
CUI.PrintSuccess(count + " Saved changes!");
CUI.Headline("Master object");
Console.WriteLine(h.ToNameValueString());
CUI.Headline("Detail object");
Console.WriteLine(d.ToNameValueString());
}
}
}
}
class MyContext : DbContext
{
public DbSet<Master> MasterSet { get; set; }
public DbSet<Detail> DetailSet { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
// Set provider and connectring string
string connstring = @"Server=.;Database=EFC_MappingScenarios_AlternateKey;Trusted_Connection=True;MultipleActiveResultSets=True;";
builder.UseSqlServer(connstring);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Alternative key with one column
modelBuilder.Entity<Detail>()
.HasAlternateKey(c => c.Guid).HasName("UniqueContraint_GuidOnly"); ;
// Alternative key with two columns
modelBuilder.Entity<Detail>()
.HasAlternateKey(c => new { c.Guid, Bereich = c.Area }).HasName("UniqueContraint_GuidAndArea");
// The Entity Framework Core automatically generates an alternate key if, in a relationship definition, you do not create the relationship between foreign key and primary key, but use a different column of the parent class instead of the primary key.
modelBuilder.Entity<Detail>()
.HasOne(p => p.Master)
.WithMany(b => b.DetailSet)
.HasForeignKey(p => p.MasterGuid)
.HasPrincipalKey(b => b.Guid);
}
}
public class Master
{
public int MasterID { get; set; }
public string Guid { get; set; }
public string Memo { get; set; }
public List<Detail> DetailSet { get; set; } = new List<Detail>();
}
public class Detail
{
public string DetailID { get; set; }
public string DetailMemo { get; set; }
public string Guid { get; set; }
public string Area { get; set; }
public string MasterGuid { get; set; }
public Master Master { get; set; }
}
}
Listing 16-12Project EFC_MappingTest, AlternateKeys.cs
Note
可选键(作为主键)不得有NULL
值。实体框架核心通过以下运行时错误确认了这一点,该错误还指出在唯一索引中允许零NULL
值:“无法跟踪' DetailKlasse '类型的实体,因为备用键属性' Guid '为空。如果在关系中没有使用替换键,那么可以考虑使用唯一索引。唯一索引可以包含空值,而备用键不能包含空值。
级联删除
如果在对象关系(1:1,1:N)中删除了一个主记录,那么一个详细数据记录(1:1)或多个详细数据记录(1:N)会发生什么情况?答案是级联删除。
删除实体框架核心中的选项
实体框架核心为级联删除依赖对象提供了总共四个选项,因此比它的前身 ADO.NET 实体框架提供了更多的选项。
如果没有相关记录的相关对象被具体化,则 Entity Framework Core 将始终向数据库发送(无论设置如何)一个针对主记录的删除命令。对详细数据集的处理取决于数据库管理系统。
然而,如果相关记录被物化为相关记录,则实体框架核心可以在删除主记录之前向数据库管理系统发送(取决于设置)一个DELETE
或UPDATE
记录详细记录命令。此外,根据设置的不同,RAM 中的外键属性也会发生变化。
表 16-3
Cascading Delete Settings and Their Effects
| 实体框架核心设置 | 默认为… | 数据库中外键的结果设置 | 运行时行为:物化相关对象 | 运行时行为:未具体化的相关记录 | | :-- | :-- | :-- | :-- | :-- | | `Cascade` | 强制关系 | `Cascade` | 实体框架核心为相关对象发送`DELETE`。RAM 中的外键属性被保留。 | DBMS 自动删除相关记录。 | | `ClientSetNull` | 可选关系(从实体框架核心 2.0 开始) | `No Action` | 实体框架核心为相关对象发送`UPDATE SET = NULL`。RAM 中的外键属性设置为`NULL`。 | 没有。如果数据库中存在记录,则出现错误。 | | `SetNull` | | `SetNull` | 实体框架核心为相关对象发送`UPDATE SET = NULL`。RAM 中的外键属性设置为`NULL`。 | DBMS 将外键列设置为`NULL`。 | | `Restrict` | 可选关系(在实体框架核心 1.x 中) | `No Action` | 版本 1.x: `UPDATE SET = NULL`。版本 2.x:内存中存在对象时出错。 | 没有。如果数据库中存在记录,则出现错误。 |实体框架核心的级联删除设置是在 Fluent API 的关系声明中使用OnDelete()
方法和DeleteBehavior
枚举进行的。
modelBuilder.Entity<Detail1>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail1Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Detail2>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail2Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.ClientSetNull);
modelBuilder.Entity<Detail3>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail3Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Detail4>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail4Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.Restrict);
看一个例子
清单 16-13 为删除选项设置了一个测试场景。有一个Master
类,包含四个不同的Detail
类的四个集合。实体框架核心的四个级联删除选项之一用于四个Detail
类中的每一个。
using ITVisions;
using ITVisions.EFCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
namespace EFC_MappingScenarios_CascadingDelete
{
/// <summary>
/// In this example, several classes are deliberately implemented in one file, so that the example is clearer.
/// </summary>
class DEMO_CascadingDelete
{
public static void Run()
{
CUI.MainHeadline(nameof(DEMO_CascasdingDelete));
using (var ctx = new MyContext())
{
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!");
}
CUI.Headline("Metadata of Master");
var obj2 = new Master();
foreach (var p in ctx.Entry(obj2).Properties)
{
Console.WriteLine(p.Metadata.Name + ": ");
}
foreach (var p in ctx.Entry(obj2).Navigations)
{
Console.WriteLine(p.Metadata.Name + ": " + p.Metadata);
}
CUI.Headline("Clean database");
ctx.Database.ExecuteSqlCommand("Delete from Detail1Set");
ctx.Database.ExecuteSqlCommand("Delete from Detail2Set");
ctx.Database.ExecuteSqlCommand("Delete from Detail3Set");
ctx.Database.ExecuteSqlCommand("Delete from Detail4Set");
ctx.Database.ExecuteSqlCommand("Delete from MasterSet");
CUI.Headline("Create one Master with three details");
var d1 = new Detail1();
var d2 = new Detail2();
var d3 = new Detail3();
var d4 = new Detail4();
var m = new Master();
m.Detail1Set.Add(d1);
m.Detail2Set.Add(d2);
m.Detail3Set.Add(d3);
//m.Detail4Set.Add(d4); // Code will fail with this
ctx.MasterSet.Add(m);
var count1 = ctx.SaveChanges();
Console.WriteLine("Saved changes: " + count1);
PrintStatusDB();
}
CUI.Headline("Delete Master object...");
using (var ctx = new MyContext())
{
var m = ctx.MasterSet.Include(x => x.Detail1Set).Include(x => x.Detail2Set).Include(x=>x.Detail3Set).FirstOrDefault();
PrintStatusRAM(m);
ctx.Log();
ctx.Remove(m);
var count2 = ctx.SaveChanges();
DbContextExtensionLogging.DoLogging = false;
Console.WriteLine("Saved changes: " + count2);
PrintStatusDB();
PrintStatusRAM(m);
}
}
private static void PrintStatusRAM(Master m)
{
Console.WriteLine("h.Detail1=" + m.Detail1Set.Count + " / Detail1.FK=" + (m.Detail1Set.Count > 0 ? m.Detail1Set.ElementAt(0)?.MasterId.ToString() : "--"));
Console.WriteLine("h.Detail2=" + m.Detail2Set.Count + " / Detail2.FK=" + (m.Detail2Set.Count > 0 ? m.Detail2Set.ElementAt(0)?.MasterId.ToString() : "--"));
Console.WriteLine("h.Detail3=" + m.Detail3Set.Count + " / Detail3.FK=" + (m.Detail3Set.Count > 0 ? m.Detail3Set.ElementAt(0)?.MasterId.ToString() : "--"));
Console.WriteLine("h.Detail4=" + m.Detail4Set.Count + " / Detail4.FK=" + (m.Detail4Set.Count > 0 ? m.Detail4Set.ElementAt(0)?.MasterId.ToString() : "--"));
}
private static void PrintStatusDB()
{
using (var ctx = new MyContext())
{
Console.WriteLine("DB Mastern: " + ctx.MasterSet.Count());
Console.WriteLine("DB Detail1: " + ctx.Detail1Set.Count());
Console.WriteLine("DB Detail2: " + ctx.Detail2Set.Count());
Console.WriteLine("DB Detail3: " + ctx.Detail3Set.Count());
Console.WriteLine("DB Detail4: " + ctx.Detail4Set.Count());
}
}
}
class MyContext : DbContext
{
public DbSet<Master> MasterSet { get; set; }
public DbSet<Detail1> Detail1Set { get; set; }
public DbSet<Detail2> Detail2Set { get; set; }
public DbSet<Detail3> Detail3Set { get; set; }
public DbSet<Detail4> Detail4Set { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
// Set provider and connectring string
string connstring = @"Server=.;Database=EFC_MappingScenarios_CascadingDelete;Trusted_Connection=True;MultipleActiveResultSets=True;";
builder.UseSqlServer(connstring);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Detail1>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail1Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Detail2>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail2Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.ClientSetNull);
modelBuilder.Entity<Detail3>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail3Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<Detail4>()
.HasOne(f => f.Master)
.WithMany(t => t.Detail4Set)
.HasForeignKey(x => x.MasterId)
.OnDelete(DeleteBehavior.Restrict);
}
}
public class Master
{
public int MasterId { get; set; }
public string Memo { get; set; }
public List<Detail1> Detail1Set { get; set; } = new List<Detail1>();
public List<Detail2> Detail2Set { get; set; } = new List<Detail2>();
public List<Detail3> Detail3Set { get; set; } = new List<Detail3>();
public List<Detail4> Detail4Set { get; set; } = new List<Detail4>();
}
public class Detail1
{
public int Detail1Id { get; set; }
public string DetailMemo { get; set; }
public Master Master { get; set; }
public int? MasterId { get; set; }
}
public class Detail2
{
public int Detail2Id { get; set; }
public string DetailMemo { get; set; }
public Master Master { get; set; }
public int? MasterId { get; set; }
}
public class Detail3
{
public int Detail3Id { get; set; }
public string DetailMemo { get; set; }
public Master Master { get; set; }
public int? MasterId { get; set; }
}
public class Detail4
{
public int Detail4Id { get; set; }
public string DetailMemo { get; set; }
public Master Master { get; set; }
public int? MasterId { get; set; }
}
}
Listing 16-13Project EFC_MappingScenarios, CascadingDelete.cs
在这种情况下,假设 RAM 中的一个Master
对象连接到前三个Detail
对象中的一个,这三个Details
对象在 RAM 中,并且主类对象已经通过调用Remove()
然后调用SaveChanges()
被删除,则发生以下反应:
DELETE
表Detail1Set
中的记录命令(级联模式)。但是,从Detail1
到 RAM 中主类对象的外键值被保留。UPDATE
表格Detail2Set
中的记录命令(在ClientSetNull
模式下)。外键值被设置为NULL
。RAM 中从Detail2
对象到Master
对象的外键值也被设置为NULL
。UPDATE
表格Detail3Set
中的记录命令(在SetNull
模式下)。外键值被设置为NULL
。RAM 中从Detail3
对象到Master
类对象的外键值也被设置为NULL
。DELETE
命令用于主类对象。
图 16-11 显示了输出。
图 16-11
Entity Framework Core has disconnected the three DetailClass objects before deleting the main class object record
如果取消对行m.Detail4Set.Add(d4)
的注释,代码将失败,并显示以下错误:“语句DELETE
与REFERENCE
约束“FK_Detail4Set_MasterSet_MasterId
”冲突。”冲突发生在数据库EFC_MappingScenarios_CascadingDelete
、表dbo.Detail4Set
和列MasterId
中,因为与Detail4
的关系处于Restrict
模式。如果对象不在 RAM 中,Detail2
(在ClientSetNull
模式下)也会发生同样的情况。
如果您取消对该行的注释,并使用.Include(x => x.Detail4Set)
将Detail4Set
加载到 RAM,则代码将失败,并显示以下错误:“实体类型‘Master’和‘detail 4’之间的关联已被切断,但此关系的外键不能设置为 null。如果应删除依赖实体,则设置关系以使用级联删除。
数据库视图的映射
实体框架核心中尚未正式支持数据库视图的映射。换句话说,不可能为现有的数据库视图创建反向工程程序代码,或者从对象模型或 Fluent API 创建正向工程数据库视图。
但是,可以在数据库中手动创建数据库视图,并将它们视为程序代码中的表。然而,这有点棘手,如本节所示。
Note
微软计划在实体框架核心 2.1 版本中为数据库视图提供更好的支持;参见附录 C 。
创建数据库视图
必须通过CREATE VIEW
在数据库中手动创建数据库视图(可能需要 SQL Server Management Studio 等工具的帮助)。清单 16-14 中的CREATE VIEW
创建的数据库视图为Flight
表提供了每次出发的航班数量和最后一次航班(来自 World Wide Wings 数据库)。
USE WWWingsV2_EN
GO
CREATE VIEW dbo.[V_DepartureStatistics]
AS
SELECT departure, COUNT(FlightNo) AS FlightCount
FROM dbo.Flight
GROUP BY departure
GO
Listing 16-14Creating a Database View via SQL Command
为数据库视图创建实体类
您必须为数据库视图创建一个实体类,其属性对应于要映射的数据库视图的列。在这个例子中,这个类被命名为DepartureStatistics
,并将接收数据库视图V_DepartureStatistics
的数据。[Table ]
注释指定了数据库中的数据库视图名称,因为实体类的名称不同。
清单 16-15 故意忽略视图V_DepartureStatistics
的LastFlight
值。重要的是,实体类需要用[Key]
或 Fluent API 方法HasKey()
指定主键。
[Table("V_DepartureStatistics")]
public class DepartureStatistics
{
[Key] // must have a PK
public string Departure { get; set; }
public int FlightCount { get; set; }
}
Listing 16-15Entity Class with Two Properties for the Two Columns of the Database View to Be Mapped
在上下文类中包含实体类
数据库视图的实体类现在通过DbSet<T>
作为表的实体类包含在上下文类中,如清单 16-16 所示。
public class WWWingsContext: DbContext
{
#region Entities for tables
public DbSet<Airline> AirlineSet { get; set; }
public DbSet<Flight> FlightSet { get; set; }
public DbSet<Pilot> PilotSet { get; set; }
public DbSet<Passenger> PassengerSet { get; set; }
public DbSet<Booking> BookingSet { get; set; }
public DbSet<AircraftType> AircraftTypeSet { get; set; }
#endregion
#region Pseudo-entities for views
public DbSet<DepartureStatistics> DepartureStatisticsSet { get; set; } // for view
#endregion
...
}
Listing 16-16Including the Entity Class for the Database View in the Context Class
使用数据库视图
现在,您可以将实体类用于数据库视图,例如用于 LINQ 查询中的表,或者用于直接 SQL 查询的FromSql()
。如果数据库视图是可写的,那么您也可以通过SaveChanges()
使用实体框架核心的 API 来更改、添加或删除记录(清单 16-17 )。
public static void DatabaseViewWithPseudoEntity()
{
CUI.MainHeadline(nameof(DatabaseViewWithPseudoEntity));
using (var ctx = new WWWingsContext())
{
var query = ctx.DepartureStatisticsSet.Where(x => x.FlightCount > 0);
var liste = query.ToList();
foreach (var stat in liste)
{
Console.WriteLine($"{stat.FlightCount:000} Flights departing from {stat.Departure}.");
}
}
}
Listing 16-17Using the Entity Class for the Database View
挑战:迁移
尽管前面几节展示了一些手工工作,但是除了一些必需的输入之外,集成数据库视图似乎并没有那么困难。不幸的是,仔细观察,这并不是故事的全部。
如果在创建数据库视图之后在 context 类中创建一个模式迁移,您会发现实体框架核心现在想要为数据库中的数据库视图创建一个表(清单 16-18 )。
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace DA.Migrations
{
public partial class v8 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "V_DepartureStatistics",
columns: table => new
{
Departure = table.Column<string>(nullable: false),
FlightCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_V_DepartureStatistics", x => x.Departure);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "V_DepartureStatistics");
}
}
}
Listing 16-18Entity Framework Core Creates a CreateTable() for the Database View in the Schema Migration Class, Which Is Not Desirable
从实体框架核心的角度来看,这是正确的,因为代码告诉 OR 映射器V_
DepartureStatistics
将是一个表,而当前版本 2.0 中的实体框架核心完全不理解数据库视图。
不幸的是,这个模式迁移无法执行,因为数据库中只能有一个名为V_DepartureStatistics
的对象。
这种情况有两种可能的解决方案,如下所述:
- 从迁移类中手动删除
Up()
方法中的CreateTable()
和Down()
中相应的DropTable()
。 - 您欺骗了实体框架核心,使得 OR 映射器在开发时创建迁移步骤时忽略实体类
DepartureStatistics
,而不是在运行时。
Tip
清单 16-19 中实现了这个技巧。作为创建或删除模式迁移的一部分,实体框架核心实例化上下文类,并调用OnModelCreating()
。但是,这不会在开发时通过应用的实际起始点发生(在这种情况下,应用会启动)。当在命令行工具ef.exe
中托管带有上下文类的 DLL 时会发生这种情况。因此,在OnModelCreating()
中,您要检查当前进程的名称是否为ef
。如果是这样,那么您不是在应用的运行时,而是在开发环境中,并且想要用Ignore(
忽略数据库视图。然而,在应用运行时,Ignore()
不会被执行,因此通过实体类使用数据库视图是可能的。
// Trick: hide the view or grouping pseudo entities from the EF migration tool so it does not want to create a new table for it
if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower() == "ef")
{
modelBuilder.Ignore<DepartureStatistics>();
...
}
Listing 16-19Entity Framework Core Should Only Ignore the Entity Class for the Database View at Development Time
Note
如果进程名称的查询太不确定,因为 Microsoft 可以更改该名称,您可以在上下文类中以静态属性的形式使用开关(例如,bool IsRuntime {get; set; } = false
)。默认情况下,这个IsRuntime
为 false,并忽略数据库视图的实体类。然而,在运行时,在上下文类第一次实例化之前,应用将IsRuntime
设置为 true。
全局查询过滤器
全局查询过滤器是实体框架核心 2.0 中一个很好的新特性。这允许您在OnModelCreating()
中集中定义过滤条件,该实体框架核心随后附加到任何 LINQ 查询、任何直接 SQL 查询、任何对表值函数的调用以及任何显式加载操作。此功能非常适合以下情况:
- 多租户:记录中的一列表示记录属于哪个租户。全局过滤器确保每个租户只能看到他们的数据。如果没有全局过滤器,您必须记住在每个查询中考虑租户条件。
- 软删除:被删除的记录不应被真正删除;它们只应该被标记。全局过滤器确保用户看不到任何“已删除”的数据。如果没有全局过滤器,您必须记住在每个查询中包含
deleted = false
条件。
定义过滤器
您使用方法HasQueryFilter()
在OnModelCreating()
中为每个实体类设置一个全局过滤器。
以下是一个全局过滤器,其中所有查询仅返回特定航空公司(即一个租户)的航班以及未完全预订的航班:
modelBuilder.Entity<Flight>().HasQueryFilter(x => x.FreeSeats > 0 && x.AirlineCode == "WWW");
Note
每个实体类最多只能定义一个过滤器。如果多次调用HasQueryFilter()
,那么只有最后一个过滤器的条件适用。要链接多个条件,使用AND
操作符(在 C# 中为&&
),如前所示。
在 LINQ 使用过滤器
前面的过滤器在数据库管理系统中强制执行这个 LINQ 查询:
List<Flight> flightSet = (from f in ctx.FlightSet //
where f.Departure == "Berlin"
select f).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].[FreeSeats] > 0) AND ([f].[AirlineCode] = N'WWW')) AND ([f].[Departure] = N'Berlin')
实体框架核心在急切加载和显式加载期间也会考虑全局过滤器,如清单 16-20 和清单 16-21 所示。
CUI.Headline("Pilots (Eager Loading)");
var pilotWithFlights= ctx.PilotSet.Include(x => x.FlightAsPilotSet).ToList();
foreach (var p in pilotWithFlights)
{
Console.WriteLine(p);
foreach (var f in p.FlightAsPilotSet.ToList())
{
Console.WriteLine(" - " + f.ToString());
}
}
Listing 16-20Explicit Load Example with Load()
CUI.Headline("Pilots (Explicit Loading)");
var pilotenSet = ctx.PilotSet.ToList();
foreach (var p in pilotenSet)
{
Console.WriteLine(p);
ctx.Entry(p).Collection(x => x.FlightAsPilotSet).Load();
foreach (var f in p.FlightAsPilotSet.ToList())
{
Console.WriteLine(" - " + f.ToString());
}
}
Listing 16-21Explicit Load Example with Load()
实际示例:忽略过滤器
您可以在每个单独的查询中决定忽略全局过滤器。这是通过IgnoreQueryFilters()
完成的,如下所示:
List<Flight> FlightAllSet = (from f in ctx.FlightSet.IgnoreQueryFilters()
where f.Departure == "Berlin"
select f).ToList();
Note
然而,不可能只忽略过滤器的个别部分。尽管这是用户所希望的,但是微软还没有提供实现。
SQL 查询的全局查询过滤器
当使用FromSql()
通过 SQL 直接查询时,全局查询过滤器(参见第十四章)也可以工作。对于只返回来自特定航空公司的航班和未完全预订的航班的全局过滤器,以下是代码中的 SQL 查询:
List<Flight> flightSet2 = ctx.FlightSet.FromSql("select * from Flight where Departure = 'Berlin'").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 (
select * from Flight where Departure = 'Berlin'
) AS [f]
WHERE ([f].[FreeSeats] > 0) AND ([f].[AirlineCode] = N'WWW')
存储过程和表值函数的全局查询筛选器
当使用表值函数(tvf)时,全局查询过滤器也与FromSql()
一起工作。
在前面的全局过滤器中,只返回航空公司的航班和未预订的航班,这个 SQL 查询
List<Flight> flightSet3 = ctx.FlightSet.FromSql("Select * from GetFlightsFromTVF({0})", "Berlin").Where(f=>f.NonSmokingFlight == true).ToList();
将导致以下结果:
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 (
Select * from GetFlightsFromTVF(@p0)
) AS [f]
WHERE (([f].[FreeSeats] > 0) AND ([f].[AirlineCode] = N'WWW')) AND ([f].[NonSmokingFlight] = 1)
Warning
当调用存储过程时,RAM 中的全局过滤器不起作用!
从以下查询:List<Flight> flightSet4 = ctx.FlightSet.FromSql("EXEC GetFlightsFromSP {0}", "Berlin").ToList();
实体框架核心将只执行以下内容:EXEC GetFlightsFromSP @p0
未来的查询
Entity Framework Plus(参见第二十章)实现了一个叫做未来查询的附加特性。这允许您通过调用Future()
扩展方法来定义一系列不立即执行的查询。这些查询可以稍后单独执行,但是这也可以与实体框架核心的标准功能一起工作。未来查询的特殊功能是,当需要查询的数据时,所有已定义的查询会一起执行,因为应用了转换或聚合运算符。
清单 16-22 首先定义了三个未来查询,然后使用ToList()
从两个查询中检索数据(所有来自柏林的飞行员和航班)。如图 16-12 所示,这三个查询都是在第一次访问数据时执行的。然后再定义两个查询(来自伦敦的航班和来自巴黎的航班)。然后,您可以访问第一个操作的第三个查询(来自罗马的航班)的数据。没有到数据库管理系统的往返,因为这些数据已经被加载。只有这时,在从伦敦来的航班上调用ToList()
时,实体框架核心才执行从伦敦和巴黎来的航班的查询。
图 16-12
Output of Listing 16-22
public static void EFPlus_FutureQuery()
{
CUI.MainHeadline(nameof(EFPlus_FutureQuery));
using (var ctx = new DA.WWWingsContext())
{
ctx.Log();
CUI.Headline("Define three future queries ... nothing happens in the database");
QueryFutureEnumerable<Pilot> qAllePilots = ctx.PilotSet.Future();
QueryFutureEnumerable<Flight> qflightSetRome = ctx.FlightSet.Where(x => x.Departure == "Rome").Future();
QueryFutureEnumerable<Flight> qFlightSetBerlin = ctx.FlightSet.Where(x => x.Departure == "Berlin").Future();
CUI.Headline("Access the pilots:");
var allePilots = qAllePilots.ToList();
Console.WriteLine(allePilots.Count + " Pilots are loaded!");
CUI.Headline("Access the flights from Rome:");
var flightSetRom = qflightSetRome.ToList();
Console.WriteLine(flightSetRom.Count + " flights from Berlin are loaded!");
CUI.Headline("Define another two future queries ... nothing happens in the database");
QueryFutureEnumerable<Flight> qFugSetLondon = ctx.FlightSet.Where(x => x.Departure == "London").Future();
QueryFutureEnumerable<Flight> qflightSetParis = ctx.FlightSet.Where(x => x.Departure == "Paris").Future();
CUI.Headline("Access the flights from Berlin:");
var flightSetBerlin = qFlightSetBerlin.ToList();
Console.WriteLine(flightSetBerlin.Count + " flights from Rome are loaded!");
CUI.Headline("Access the flights from London:");
var flightSetLondon = qFugSetLondon.ToList();
Console.WriteLine(flightSetLondon.Count + " flights from London are loaded!");
CUI.Headline("Access the flights from Paris:");
var flightSetParis = qflightSetParis.ToList();
Console.WriteLine(flightSetParis.Count + " flights from Paris are loaded!");
}
}
Listing 16-22Future Queries with EFPlus
十七、性能调优
本章提供了使用实体框架核心加速数据库访问的指导。
实体框架核心中性能优化的过程模型
与经典的实体框架一样,以下用于优化数据库访问性能的流程模型已经在实体框架核心中得到证明:
- 第一步是使用实体框架核心和 LINQ 实现所有数据访问,除了
UPDATE
、DELETE
和INSERT
批量操作。批量操作直接使用 SQL 或批量插入。 - 然后用真实的数据集测试应用。速度太慢的地方,分三个阶段优化。
- 在阶段 1 中,使用了实体框架核心中的技巧,例如无跟踪、缓存、分页、改变加载策略(例如,急切加载或预加载而不是显式加载),以及减少往返次数。
- 如果这还不够,在第 2 阶段,将检查 LINQ 命令,并替换为优化更好的 SQL 命令或其他数据库结构,如视图、存储过程或表值函数(tvf)。通过实体框架核心继续访问它们。
- 只有在阶段 3 中,实体框架核心才被用于访问 SQL 命令、视图、存储过程和 tvf 的
DataReader
和Command
对象所取代。
您自己的性能测试的最佳实践
如果您正在执行性能测试以检查不同替代方案的速度,请考虑以下事项:
- 不要在 Visual Studio 中运行性能测试。调试器和可能激活的 IntelliTrace 功能会以不同的方式降低程序代码的速度,这取决于过程,并且您不会收到绝对正确或按比例正确的结果。
- 不要在图形用户界面(GUI)应用中运行性能测试,也不要进行任何控制台输出。为性能测试编写一个控制台应用,但是不要将任何内容打印到控制台,并且不要将控制台输出的时间包括在您的度量中。
- 每个测试重复几次,至少十次,计算平均值。有许多因素会影响结果(例如。NET 垃圾收集和 Windows 分页文件)。
- 不要将第一次运行(冷启动)包括在平均值中。实体框架核心和数据库管理系统的额外任务必须在第一次运行时完成(例如,启动数据库、建立连接、生成映射代码等等),这会伪造您的结果。如果你想得到十个有效结果,就做十一次。
- 在不同的系统上测试远程数据库管理系统(除非您的解决方案确实使用本地数据库)。
- 确保测试机器在测试过程中不执行任何其他重要的进程,并且网络不明显活跃(当其他人都在家时运行测试,或者设置您自己的网络!).
中各种数据访问技术的性能比较。网
当开发实体框架核心时,微软的目标是平台独立性和在经典的 ADO.NET 实体框架上提高性能。本章涵盖了许多性能测试场景中的一个。
图 17-1 显示实体框架核心 1.1。无跟踪模式下的. NET Framework 4.7 几乎与手动映射的DataReader
一样快(换句话说,从DataReader
复制到. NET 对象:obj.x = Convert (dr ["x"])
)。
图 17-1
Performance comparison of various data access techniques in .NET
和。NET Core 2.0,无论是手动映射还是实体框架核心都比。NET 框架 4.7。有趣的是,实体框架核心的无跟踪模式大大受益于。NET Core 2.0,相当于在。网芯 2.0。
同样在跟踪模式下,实体框架核心比 ADO.NET 实体框架 6.x 更快
最快的测量(22 毫秒。NET Framework 4.7 或 21 毫秒。NET Core 2.0)来自于一个DataReader
,但是数据集没有被映射到对象。
请注意以下关于此测量场景的内容:
- 数据库服务器在 Windows Server 2016 上运行 Microsoft SQL Server 2016(虚拟化)。如果数据库服务器没有被虚拟化,或者至少可以使用非虚拟化的硬盘空间,那么它的性能会更好。
- 客户端是 Windows 10 机器。
- 两台计算机通过 1GB 以太网连接。
- 执行的命令是一个简单的
SELECT
,没有连接或聚合操作符。 - 加载了 10,000 条记录。
- 结果记录由单个表中的 13 列组成。
- 数据类型有
int
、smallint
、nvarchar(30)
、nvarchar(max)
、bit
和timestamp
。 - 显示的值是 100 次重复的平均值。
- 每种技术(冷启动)的第一次访问不包括在平均值中。
Note
当然,这只是众多可能的对比场景之一。由于性能取决于硬件、操作系统、软件版本,尤其是数据库模式,因此在这一点上记录进一步的性能比较没有意义。无论如何,您必须在您的特定场景中衡量性能。
优化对象分配
要分配相关对象,实体框架核心为您提供了两个选项。
- 通过对象引用的赋值(清单 17-1
- 通过外键属性的赋值(清单 17-2
public static void ChangePilotUsingObjectAssignment()
{
CUI.Headline(nameof(ChangePilotUsingObjectAssignment));
var flightNo = 102;
var newPilotID = 123;
using (var ctx = new WWWingsContext())
{
ctx.Log();
Flight flight = ctx.FlightSet.Find(flightNo);
Pilot newPilot = ctx.PilotSet.Find(newPilotID);
flight.Pilot = newPilot;
var count = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count);
}
}
Listing 17-1Assignment via an Object Reference
public static void ChangePilotUsingFK()
{
CUI.Headline(nameof(ChangePilotUsingFK));
var flightNo = 102;
var newPilotID = 123;
using (var ctx = new WWWingsContext())
{
ctx.Log();
Flight flight = ctx.FlightSet.Find(flightNo);
flight.PilotId = newPilotID;
var count = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count);
}
}
Listing 17-2Assignment via a Foreign Key Property
使用外键属性会更有效一些,因为不必显式加载对象。实体框架核心发送给数据库的UPDATE
命令在两种情况下看起来是一样的(见图 17-2 )。
图 17-2
Output of Listing 17-2
如果要分配的对象已经在 RAM 中,您可以在两种语法形式之间进行选择。但是如果在 RAM 中只有要分配的对象的主键值,那么应该使用外键属性。这种情况在 web 应用和 web 服务中很常见,因为客户端只获得主键值。
Tip
外键属性在实体框架核心中是可选的。然而,赋值中的优化选项是除了实体类中的导航属性之外,还应该实现外键属性的一个很好的理由!
虽然没有外键属性,但是您可以通过使用实体框架核心中外键列的 shadow 属性来绕过加载对象(参见清单 17-3 )。然而,这个过程的缺点是外键列的名称必须在程序代码中作为字符串使用,这在输入时更麻烦,也更容易出错。
public static void ChangePilotUsingFKShadowProperty()
{
CUI.Headline(nameof(ChangePilotUsingFKShadowProperty));
var flightNo = 102;
var neuerPilotNr = 123;
using (var ctx = new WWWingsContext())
{
ctx.Log();
Flight flight = ctx.FlightSet.Find(flightNo);
ctx.Entry(flight).Property("PilotId").CurrentValue = neuerPilotNr;
var count = ctx.SaveChanges();
Console.WriteLine("Number of saved changes: " + count);
}
}
Listing 17-3Assignment via the Shadow Key Property of the Foreign Key Column
批量操作
Entity Framework Core 在其默认配置中不支持对多条记录进行批量操作,而是单独处理每条记录以进行删除和修改。本章讨论了对 1,000 条记录使用删除命令(DELETE
)的主题。该信息也适用于使用UPDATE
的批量数据变更。
单次删除
清单 17-4 显示了一种从Flight
表中删除主键的值大于 20,000 的所有记录的低效方法。必须首先将所有记录加载并具体化到。NET 对象。每个。然后,使用Remove()
方法将. NET 对象标记为删除,当对每个对象执行SaveChanges()
方法时,删除最终被实体框架核心上下文转换为DELETE
命令。
所以,如果要删除 1000 个航班,就需要 1001 个命令(1 个SELECT
和 1000 个DELETE
命令)。在实现中,1000 个DELETE
命令,一次一个,被发送到数据库管理系统,因为SaveChanges()
方法出现在循环中每个单独的Remove()
之后。
public static void BulkDeleteEFCAPIwithoutBatching()
{
CUI.Headline(nameof(BulkDeleteEFCAPIwithoutBatching));
var sw = new Stopwatch();
sw.Start();
int total = 0;
using (var ctx = new WWWingsContext())
{
var min = 20000;
var flightSet = ctx.FlightSet.Where(x => x.FlightNo >= min).ToList();
foreach (Flight f in flightSet)
{
ctx.FlightSet.Remove(f);
var count = ctx.SaveChanges();
total += count;
}
}
sw.Stop();
Console.WriteLine("Number of DELETE statements: " + total);
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-4Bulk Clear Without Batching with the Entity Framework Core API
批处理优化
在经典的实体框架中,在每个Remove()
之后SaveChanges()
是在循环中还是在循环之外并不重要。旧的 OR 映射器总是一次一个地将每个DELETE
命令传输到数据库管理系统。在实体框架核心中,有批处理(见第章第十部分)来缓解这个问题。
清单 17-5 ,它在最后只执行SaveChanges()
一次,因此不会导致 1000 次DELETE
到数据库管理系统的往返,而是只有一次(见图 17-3 )。总共只剩下两个往返行程(一个用于加载SELECT
,一个用于 1000 个DELETE
命令)。执行时间显著减少(见表 17-1 )。
表 17-1
Execution Time
| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 |图 17-3
Here, 1,000 delete commands are transferred in one round-trip to the database management system
public static void BulkDeleteEFCAPIwithBatching()
{
CUI.Headline(nameof(BulkDeleteEFCAPIwithBatching));
int total = 0;
var sw = new Stopwatch();
sw.Start();
using (var ctx = new WWWingsContext())
{
var min = 20000;
var flightSet = ctx.FlightSet.Where(x => x.FlightNo >= min).ToList();
foreach (Flight f in flightSet)
{
ctx.FlightSet.Remove(f);
}
total = ctx.SaveChanges();
}
sw.Stop();
Console.WriteLine("Number of DELETE statements: " + total);
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-5Bulk Deleting Batching with the Entity Framework Core API
删除而不加载伪对象
即使使用批处理,这种操作仍然是低效的,因为最初所有记录都是在删除后才加载的。运行实体框架时,核心需要一个实体对象。
这里有一个技巧,在 RAM 中手动构造这样一个实体对象作为伪对象,并将其作为参数传递给Remove()
(参见清单 17-6 )。这显著提高了性能(见表 17-2 )。然而,这只有在以下两种情况下才有可能:
- 要删除的对象的主键是已知的。
- 没有通过
[ConcurrencyCheck]
运行的并发检查,没有IsConcurrencyToken()
方法,也没有时间戳列。
public static void BulkDeleteEFCAPIusingPseudoObject()
{
CUI.Headline(nameof(BulkDeleteEFCAPIusingPseudoObject));
int total = 0;
var sw = new Stopwatch();
sw.Start();
using (var ctx = new WWWingsContext())
{
for (int i = 20001; i < 21000; i++)
{
var f = new Flight() { FlightNo = i };
ctx.FlightSet.Attach(f);
ctx.FlightSet.Remove(f);
}
total = ctx.SaveChanges();
}
sw.Stop();
Console.WriteLine("Number of DELETE statements: " + total);
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-6Bulk Deleting Batching with Entity Framework Core API Using Pseudo-Objects
表 17-2
Execution Time
| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 0.157 秒 |使用经典 SQL 代替实体框架核心 API
通过迄今为止采取的措施,执行时间已经大大缩短,但时间仍然比必要的长得多。这里不使用实体框架核心 API,在要删除一组连续记录的情况下,发出一个简单的经典 SQL 命令要有效得多。
DELETE dbo.Flight where FlightNo >= 20000
您可以通过带有Parameter
对象的 ADO.NET 命令以传统方式设置这个 SQL 命令(参见清单 17-7 ),或者更简洁地通过实体框架核心中的直接 SQL 支持(参见清单 17-8 )使用实体框架核心上下文的Database
子对象中的ExecuteSqlCommand()
方法来设置这个 SQL 命令。应该强调的是,基于string.Format()
的通配符语法可以防止 SQL 注入攻击,如清单 17-7 中的参数化所示。这里,字符串不是简单地放在一起,如语法所示,而是在内部生成 SQL 参数对象。
在这两种情况下,执行时间都减少到不到 40 ms,这并不奇怪,因为程序现在只需要建立一个数据库连接和传输一些字符。无法测量通过实体框架核心上下文或Command
对象的传输之间的性能差异。
当然,这种方法的缺点是再次使用 SQL 字符串,对此没有编译器检查,因此存在语法和类型错误的风险,直到运行时才被注意到。
| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 0.157 秒 | | 通过实体框架上下文使用 SQL 批量删除 1,000 条`Flight`记录 | one | 0.034 秒 | | 通过带有参数的 ADO.NET 命令对象使用 SQL 批量删除 1000 条`Flight`记录 | one | 0.034 秒 |public static void BulkDeleteADONETCommand()
{
CUI.Headline(nameof(BulkDeleteADONETCommand));
int total = 0;
var min = 20000;
var sw = new Stopwatch();
sw.Start();
using (SqlConnection connection = new SqlConnection(Program.CONNSTRING))
{
connection.Open();
SqlCommand command = new SqlCommand("DELETE dbo.Flight where FlightNo >= @min", connection);
command.Parameters.Add(new SqlParameter("@min", min));
total = command.ExecuteNonQuery();
}
sw.Stop();
Console.WriteLine("Number of DELETE statements: " + total);
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-7Bulk Delete with SQL via ADO.NET Command Object with a Parameter
public static void BulkDeleteEFCSQL()
{
CUI.Headline(nameof(BulkDeleteEFCSQL));
int total = 0;
var min = 20000;
var sw = new Stopwatch();
sw.Start();
using (var ctx = new WWWingsContext())
{
total = ctx.Database.ExecuteSqlCommand("DELETE dbo.Flight where FlightNo >= {0}", min);
}
sw.Stop();
Console.WriteLine("Number of DELETE statements: " + total);
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-8Bulk Erase with SQL via Entity Framework Core Context
使用 EFPlus 进行质量删除的 Lambda 表达式
扩展组件 EFPlus(参见第二十章)允许基于 LINQ 命令在 lambda 表达式中公式化UPDATE
和DELETE
命令,这样可以避免容易出错的 SQL。
EFPlus 组件实现了名为Update()
和Delete()
的扩展方法。要实现这一点,使用Z.EntityFramework.Plus
是必要的。
清单 17-9 展示了如何在 LINQ 命令中使用Delete()
。不幸的是,如图 17-4 所示,EFPlus 生成的 SQL 命令并不理想。他们总是使用嵌套的SELECT
,尽管这不是必须的。从 EFPlus 作者的角度来看,这是最简单的实现,因为很容易使用现有的SELECT
命令生成实体框架核心。执行结果与使用直接 SQL 命令的结果相同(见表 17-3 )。
表 17-3
Execution Time
| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 157 秒 | | 通过实体框架上下文使用 SQL 批量删除 1,000 条`Flight`记录 | one | 34 秒 | | 通过带有参数的 ADO.NET 命令对象使用 SQL 批量删除 1000 条`Flight`记录 | one | 34 秒 | | 使用 EFPlus 批量删除 1,000 条`Flight`记录 | one | 45 秒 |图 17-4
SQL DELETE command that EFPlus sends to the database management system
public static void BulkDeleteEFPlus()
{
CUI.Headline(nameof(BulkDeleteEFPlus));
int min = 20000;
int total = 0;
var sw = new Stopwatch();
sw.Start();
using (var ctx = new WWWingsContext())
{
var count = ctx.FlightSet.Where(x => x.FlightNo >= min).Delete();
Console.WriteLine("Number of DELETE statements: " + count);
}
sw.Stop();
Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
Timer_BulkDeleteEFPlus += sw.ElapsedMilliseconds;
}
Listing 17-9Mass Deletion with EFPlus
使用 EFPlus 批量更新
清单 17-10 显示了使用扩展方法Update()
的附加组件 EFPlus(参见第二十章)制定的UPDATE
命令,该命令将从柏林出发的未来航班的免费座位数减少一个。图 17-5 显示了发送到数据库管理系统的 SQL UPDATE
命令,其工作方式与Delete()
相同。
图 17-5
SQL UPDATE command that EFPlus sends to the database management system
public static void BulkUpdateEFPlus()
{
CUI.Headline(nameof(BulkUpdateEFPlus));
using (var ctx = new WWWingsContext())
{
var count = ctx.FlightSet.Where(x => x.Departure == "Berlin" && x.Date >= DateTime.Now).Update(x => new Flight() { FreeSeats = (short)(x.FreeSeats - 1) });
Console.WriteLine("Changed records: " + count);
}
}
Listing 17-10Bulk Update with EFPlus
通过无跟踪实现性能优化
与它的前身 ADO.NET 实体框架一样,实体框架核心具有无跟踪模式,这大大加快了数据记录的加载速度。在新的实现中,微软通过添加上下文选项改进了该功能的实际应用。
图 17-6 中的性能测量显示,可选的无跟踪模式比标准跟踪模式提供了显著的速度优势——在传统的 ADO.NET 实体框架和新的实体框架核心中都是如此。在无跟踪模式下,实体框架核心可以在 46 毫秒内通过网络获取 10,000 条记录(来自一个表的 13 列,无连接、int
、smallint
、nvarchar(30)
、nvarchar (max)
、bit
、timestamp)
,并将它们在 RAM 中具体化为对象。这几乎和手动映射的 ADO.NETDataReader
一样快(比如obj.Name = Convert.ToString(dataReader["name"])
这样的自写代码行)。在正常跟踪模式下,读取记录需要两倍多一点的时间(100 毫秒)。
图 17-6
Performance comparison
相比之下,图 17-6 也显示了 Entity Framework 6.x,这里在跟踪模式下需要 263 ms。在 53 毫秒时,与非跟踪模式下的实体框架核心相比,只有微小的差异。微软因此相对于 Entity Framework 6.1.3 加速了 Entity Framework Core,尤其是跟踪模式。尽管如此,实体框架核心中的无跟踪模式也有好处。
激活无跟踪模式
在经典实体框架的第一个版本中,您必须使用属性MergeOption
为每个实体类或每个查询设置无跟踪模式,并附加一行代码。从 Entity Framework 4.1 开始,您可以使用更加优雅的AsNoTracking()
扩展方法在查询级别设置模式(参见清单 17-11 )。在实体框架核心中,只有AsNoTracking()
用于此。
CUI.Headline("No-Tracking mode");
using (WWWingsContext ctx = new WWWingsContext())
{
var flightSet = ctx.FlightSet.AsNoTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
flight.FreeSeats--;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 0
}
Listing 17-11Activation of No-Tracking Mode with AsNoTracking( ) in Entity Framework 6.x and Entity Framework Core
无跟踪模式的结果如图 17-7 所示,输出列表 17-11 。如果激活非跟踪模式,实体框架核心的更改跟踪功能将不再有效。在默认情况下,对象在加载后处于状态Unchanged
,并且它们在改变后改变到状态Modified
。当在无跟踪模式下加载时,它们在加载后是Detached
,并且即使在改变后也保持如此。然后,SaveChanges()
方法的执行不向数据库管理系统发送任何改变,因为实体框架核心没有注意到该改变。
图 17-7
Screen output from Listing 17-11
无跟踪模式几乎总是可行的
在任何情况下,不跟踪模式应该总是用于只显示数据的对象,不应该进行任何修改。但是,即使您想要修改单个对象,您也可以首先以非跟踪模式加载对象,然后再将它们附加到上下文类。因此,您只需更改对象——最好是在更改之前——并使用Attach()
方法将它们添加到上下文中。这个方法同时存在于DbContext
类和dbSet<T>
类中。
Attach()
方法将一个对象添加到实体框架核心变更跟踪中。物体由此从状态Detached
转移到状态Unchanged
。当然,只有实体类的实例可以传递给Attach()
。如果您传递实体框架核心上下文不知道的类的实例,您将得到下面的错误消息:“找不到实体类型 xy。请确保该实体类型已添加到模型中。
清单 17-12 (以及图 17-8 中附带的屏幕输出)显示了Attach()
方法在这三个场景中的使用:
Attach()
是在实际变更之前执行的。在这种情况下,没有其他事情要做,因为实体框架核心识别出Attach()
之后的所有变化,并将对象独立地从状态Unchanged
转移到状态Modified
。- 如果在执行
Attach()
之前发生了变化,那么实体框架核心对发生的变化一无所知。因此,您必须随后向ctx.Entry(obj).Property (f => f.Property).IsModified = true
登记变更。 - 如果您不知道对象的已更改属性(例如,因为更改发生在调用程序代码或另一个进程中),或者将单个属性设置为
IsModified
太麻烦,您可以使用ctx.Entry(Flight).State = EntityState.Modified
来设置整个对象的状态。
public static void TrackingMode_NoTracking_Attach()
{
CUI.MainHeadline(nameof(TrackingMode_NoTracking_Attach));
CUI.Headline("Attach() before change");
using (WWWingsContext ctx = new WWWingsContext())
{
var flightSet = ctx.FlightSet.AsNoTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
ctx.Attach(flight);
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
flight.FreeSeats--;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 0
}
CUI.Headline("Attach() after change (change state per property)");
using (WWWingsContext ctx = new WWWingsContext())
{
var flightSet = ctx.FlightSet.AsNoTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
flight.FreeSeats--;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
ctx.Attach(flight);
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
// Register changed property at EFC
ctx.Entry(flight).Property(f => f.FreeSeats).IsModified = true;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 1
}
CUI.Headline("Attach() after change (change state per object)");
using (WWWingsContext ctx = new WWWingsContext())
{
var flightSet = ctx.FlightSet.AsNoTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
flight.FreeSeats--;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
ctx.Attach(flight);
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
ctx.Entry(flight).State = EntityState.Modified;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 1
}
}
Listing 17-12Using the Attach( ) Method
如图 17-8 所示,在所有三种情况下,SaveChanges()
都会保存更改。然而,在幕后,这三个场景是有区别的。在前两个场景中,实体框架核心向数据库发送一个 SQL UPDATE
命令,该命令只更新实际的Free Spend
列。
exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1;
图 17-8
Output from Listing 17-12
然而,在第三个场景中,开发人员没有给出核心实体框架关于哪些属性实际上发生了变化的信息。实体框架核心不得不将所有属性的值发送回数据库,即使这些值在那里已经是已知的。
UPDATE [Flight] SET [AircraftTypeID] = @p0, [AirlineCode] = @p1, [CopilotId] = @p2, [FlightDate] = @p3, [Departure] = @p4, [Destination] = @p5, [FreeSeats] = @p6, [LastChange] = @p7, [Memo] = @p8, [NonSmokingFlight] = @p9, [PilotId] = @p10, [Price] = @p11, [Seats] = @p12, [Strikebound] = @p13
WHERE [FlightNo] = @p14 AND [Timestamp] = @p15;
SELECT [Timestamp], [Utilization]
FROM [Flight]
WHERE @@ROWCOUNT = 1 AND [FlightNo] = @p14;
Note
除了通过线路发送不必要的数据这一事实之外,更新所有列还会造成潜在的数据更改冲突。如果其他进程已经更改了部分记录,这些更改将无情地覆盖其他进程。因此,您应该始终确保实体框架核心知道已更改的列。如果对象的修改发生在调用者的Attach()
方法之前,那么调用者必须提供相应的关于改变的属性的元信息。
可编辑数据网格中的无跟踪模式
当使用方法Attach()
时,您可以在无跟踪模式下加载几乎所有的记录。图 17-9 和图 17-10 显示了通常的数据网格场景。用户可以加载(更多)数据并更改任何数据集。然后通过单击保存来保存更改。(见图 17-12 )。
图 17-10
10,000 records loaded in no-tracking mode in 96 milliseconds
图 17-9
10,000 records loaded in tracking mode in 174 milliseconds
在这种情况下,完全没有必要浪费加载时跟踪模式的额外时间。使用Attach()
来记录用户在实体框架上下文中工作的单个记录就足够了(参见清单 17-13 )。微软为 Windows 演示基金会(WPF)提供的 DataGrid 控件使用了BeginningEdit()
事件。在事件处理程序中,Attach()
将分离的对象转移到一个附加的对象(见图 17-11 ),从而将该对象注册为实体框架上下文变更跟踪的一部分。
然而,在用AsNoTracking()
加载之后,用Attach(). Attach()
在一个循环中附加所有对象花费每个对象不到一毫秒的时间并不是一个好主意。当您将单个对象附加到它时,这一点并不明显。但总的来说,这样一个循环比在跟踪模式下直接加载所有对象要慢。所以如果你确定所有的对象都必须被改变,你应该在加载的时候使用跟踪模式。
/// <summary>
/// Called when starting to editing a flight in the grid
/// </summary>
private void C_flightDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
// Access to the current edited Flight
var flight = (Flight)e.Row.Item;
if (flight.FlightNo > 0) // important so that new flights are not added before filling
{
// Attach may only be done if the object is not already attached!
if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
{
ctx.FlightSet.Attach(flight);
SetStatus($"Flight {flight.FlightNo} can now be edited!");
}
}
}
Listing 17-13Attaching an Object to the Context When the User Starts Editing
图 17-11
The developer logs the object to be changed in the DataGrid to Attach( ) at the Entity Framework context when the change begins Note
只有当对象尚未连接到上下文实例时,才能执行Attach()
。否则,将会出现运行时错误。对象是否已经连接到上下文,您不能询问对象本身。然而,DbSet<T>
类有一个名为Local
的属性,它包含了实体框架核心的本地缓存中的所有对象。要查询这个缓存,请使用ctx.FlightSet.Local.Any (x => x.FlightNo == flight.FlightNo)
。
Warning
属性Local
有一个方法Clear()
。正如您所料,这不仅会清空实体框架核心上下文的缓存,还会将其中包含的所有对象置于Deleted
状态,这将在下一个SaveChanges()
删除它们!要真正只从缓存中删除对象,您必须将对象单独设置为状态Detached
,如下所示:
foreach (var f in ctx.FlightSet.Local.ToList ())
{
ctx.Entry (f) .State = EntityState.Detached;
}
图 17-12
Saving changes although loaded in no-tracking mode
清单 17-14 和清单 17-15 显示了 XAML 代码和完整的代码隐藏类。
<Window x:Class="GUI.WPF.FlightGridNoTracking"
xmlns:="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GUI.WPF"
xmlns:wpf="clr-namespace:ITVisions.WPF;assembly=ITV_DemoUtil"
mc:Ignorable="d"
Title="World Wide Wings - FlightGridNoTracking" Height="455.233" Width="634.884">
<Window.Resources>
<wpf:InverseBooleanConverter x:Key="InverseBooleanConverter"></wpf:InverseBooleanConverter>
</Window.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<DockPanel>
<!--===================== Command Bar->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
<ComboBox Width="100" x:Name="C_City" ItemsSource="{Binding}">
<ComboBoxItem Content="All" IsSelected="True" />
<ComboBoxItem Content="Rome" />
<ComboBoxItem Content="Paris" />
<ComboBoxItem Content="New York/JFC" />
<ComboBoxItem Content="Berlin" />
</ComboBox>
<ComboBox Width="100" x:Name="C_Count" >
<ComboBoxItem Content="10" IsSelected="True" />
<ComboBoxItem Content="100" IsSelected="True" />
<ComboBoxItem Content="1000" IsSelected="True" />
<ComboBoxItem Content="All" IsSelected="True" />
</ComboBox>
<ComboBox Width="100" x:Name="C_Mode" >
<ComboBoxItem Content="Tracking" IsSelected="True" />
<ComboBoxItem Content="NoTracking" IsSelected="False" />
</ComboBox>
<Button Width="100" x:Name="C_Test" Content="Test Connection" Click="C_Test_Click" ></Button>
<Button Width="100" x:Name="C_Load" Content="Load" Click="C_Load_Click"></Button>
<Button Width="100" x:Name="C_Save" Content="Save" Click="C_Save_Click"></Button>
</StackPanel>
<!-===================== Status Bar->
<StatusBar DockPanel.Dock="Bottom">
<Label x:Name="C_Status"></Label>
</StatusBar>
<!-===================== Datagrid->
<DataGrid Name="C_flightDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True" IsSynchronizedWithCurrentItem="True" SelectedIndex="0" Height="Auto" BeginningEdit="C_flightDataGrid_BeginningEdit" PreviewKeyDown="C_flightDataGrid_PreviewKeyDown" RowEditEnding="C_flightDataGrid_RowEditEnding">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=FlightNo}" Header="Flight No" Width="SizeToHeader" />
<DataGridTextColumn Binding="{Binding Path=Departure}" Header="Departure" Width="SizeToHeader" />
<DataGridTextColumn Binding="{Binding Path=Destination}" Header="Destination" Width="SizeToHeader" />
<DataGridTextColumn Binding="{Binding Path=Seats}" Header="Seats" Width="SizeToHeader" />
<DataGridTextColumn Binding="{Binding Path=FreeSeats}" Header="Free Seats" Width="SizeToHeader" />
<DataGridCheckBoxColumn Binding="{Binding Path=NonSmokingFlight, Converter={StaticResource InverseBooleanConverter}}" Header="Non Smoking Flight" Width="SizeToHeader" />
<DataGridTemplateColumn Header="Date" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding Path=Date}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding Path=Memo}" Width="200" Header="Memo" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</Window>
Listing 17-14XAML Code FlightGridNoTracking.xaml (Project EFC_GUI)
using BO;
using DA;
using Microsoft.EntityFrameworkCore;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace GUI.WPF
{
public partial class FlightGridNoTracking : Window
{
public FlightGridNoTracking()
{
InitializeComponent();
this.Title = this.Title + "- Version: " + Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
private void SetStatus(string s)
{
this.C_Status.Content = s;
}
WWWingsContext ctx;
/// <summary>
/// Load flights
/// </summary>
private void C_Load_Click(object sender, RoutedEventArgs e)
{
ctx = new WWWingsContext();
// Clear grid
this.C_flightDataGrid.ItemsSource = null;
// Get departure
string Ort = this.C_City.Text.ToString();
// Show status
SetStatus("Loading with " + this.C_Mode.Text + "...");
// Prepare query
var q = ctx.FlightSet.AsQueryable();
if (this.C_Mode.Text == "NoTracking") q = q.AsNoTracking();
if (Ort != "All") q = (from f in q where f.Departure == Ort select f);
if (Int32.TryParse(this.C_Count.Text, out int count))
{
if (count>0) q = q.Take(count);
}
var sw = new Stopwatch();
sw.Start();
// Execute query
var fluege = q.ToList();
sw.Stop();
// Databinding to grid
this.C_flightDataGrid.ItemsSource = fluege; // Local is empty at NoTracking;
// set state
SetStatus(fluege.Count() + " loaded records using " + this.C_Mode.Text + ": " + sw.ElapsedMilliseconds + "ms!");
}
/// <summary>
/// Save the changed flights
/// </summary>
private void C_Save_Click(object sender, RoutedEventArgs e)
{
// Get changes and ask
var added = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Added select x;
var del = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Deleted select x;
var mod = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Modified select x;
if (MessageBox.Show("Do you want to save the following changes?\n" + String.Format("Client: Changed: {0} New: {1} Deleted: {2}", mod.Count(), added.Count(), del.Count()), "Confirmation", MessageBoxButton.YesNo) == MessageBoxResult.No) return;
string Ergebnis = "";
// Save
Ergebnis = ctx.SaveChanges().ToString();
// Show status
SetStatus("Number of saved changes: " + Ergebnis);
}
/// <summary>
/// Called when starting to editing a flight in the grid
/// </summary>
private void C_flightDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
// Access to the current edited Flight
var flight = (Flight)e.Row.Item;
if (flight.FlightNo > 0) // important so that new flights are not added before filling
{
// Attach may only be done if the object is not already attached!
if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
{
ctx.FlightSet.Attach(flight);
SetStatus($"Flight {flight.FlightNo} can now be edited!");
}
}
}
/// <summary>
/// Called when deleting a flight in the grid
/// </summary>
private void C_flightDataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
var flight = (Flight)((DataGrid)sender).CurrentItem;
if (e.Key == Key.Delete)
{
// Attach may only be done if the object is not already attached!
if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
{
ctx.FlightSet.Attach(flight);
}
ctx.FlightSet.Remove(flight);
SetStatus($"Flight {flight.FlightNo} can be deleted!");
}
}
/// <summary>
/// Called when adding a flight in the grid
/// </summary>
private void C_flightDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
var flight = (Flight)e.Row.Item;
if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
{
ctx.FlightSet.Add(flight);
SetStatus($"Flight {flight.FlightNo} has bee added!");
}
}
private void C_Test_Click(object sender, RoutedEventArgs e)
{
try
{
ctx = new WWWingsContext();
var flight = ctx.FlightSet.FirstOrDefault();
if (flight == null) MessageBox.Show("No flights :-(", "Test Connection", MessageBoxButton.OK, MessageBoxImage.Warning);
else MessageBox.Show("OK!", "Test Connection", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show("Error: " + ex.ToString(), "Test Connection", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
Listing 17-15Code-Behind Class FlightGridNoTracking.cs (Project EFC_GUI)
QueryTrackingBehavior 和 AsTracking()
当使用实体框架和实体框架核心数据集读取数据时,无跟踪模式显著提高了性能。您在前面的章节中已经看到,几乎应该总是使用无跟踪模式。不幸的是,在经典的实体框架中,您必须记住在每个查询中使用AsNoTracking()
。这不仅令人讨厌,而且很容易忘记。在传统的实体框架中,您需要额外的解决方案,例如访问DbSet<T>
的抽象,它每次都会自动启用无跟踪模式。
在实体框架核心中,微软引入了一个更优雅的解决方案:你可以将整个实体框架核心上下文置于无跟踪模式。子对象ChangeTracker
的类Microsoft.EntityFrameworkCore.DbContext
中有枚举属性QueryTrackingBehavior
。默认设置为QueryTrackingBehavior.TrackAll
;换句话说,跟踪被激活。但是,如果您将其更改为QueryTrackingBehavior.NoTracking
,所有查询都将以无跟踪模式执行,即使没有AsNoTracking()
扩展方法。为了在跟踪模式下执行单个查询,对于非跟踪基本模式有一个新的扩展方法AsTracking()
(参见清单 17-16 )。图 17-13 显示了输出。
图 17-13
Output of Listing 17-16
public static void TrackingMode_QueryTrackingBehavior()
{
CUI.MainHeadline("Default setting: TrackAll. Use AsNoTracking()");
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; // Standard
var flightSet = ctx.FlightSet.AsNoTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
flight.FreeSeats-;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 0
}
CUI.MainHeadline("Default setting: NoTracking.");
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // NoTracking
var flightSet = ctx.FlightSet.ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
flight.FreeSeats-;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 0
}
CUI.MainHeadline("Default setting: NoTracking. Use AsTracking()");
using (WWWingsContext ctx = new WWWingsContext())
{
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // NoTracking
var flightSet = ctx.FlightSet.AsTracking().ToList();
var flight = flightSet[0];
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
flight.FreeSeats-;
Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
int count = ctx.SaveChanges();
Console.WriteLine($"Saved changes: {count}"); // 1
}
}
Listing 17-16Setting QueryTrackingBehavior and Using AsTracking( )
无跟踪模式的后果
除了缺少更改跟踪之外,无跟踪模式还有其他后果,如下所示:
- 对象不会加载到实体框架核心的一级缓存中。当再次访问对象时(例如用
DbSet<T>.Find()
),它总是被数据库管理系统加载。 - 没有关系修复。关系修复是实体框架核心的一个特性,如果根据数据库,两个独立加载的对象属于同一个对象,那么它会将这两个对象连接起来。例如,假设
Pilot
123 已经被加载。Flight
101 现在被加载,并且在其外键关系中Pilot
的值为 123。实体框架核心将连接 RAM 中的Flight
101 和Pilot
123,以便您可以从Flight
导航到Pilot
,并且对于双向导航,从Pilot
导航到Flight
。 - 延迟加载不支持无跟踪,但是无论如何,延迟加载目前在实体框架核心中是不可用的。
最佳实践
新的默认QueryTrackingBehavior.NoTracking
和新的扩展方法AsTracking()
是实体框架核心中有意义的添加。但是在实践中已经看到了许多表现不佳的实体框架/实体框架核心应用,对我来说这还不够。QueryTrackingBehavior.NoTracking
应该是标准的,以便所有开发人员得到高性能的查询执行。目前,在以QueryTrackingBehavior.TrackAll
为标准设置的实体框架核心中,每个开发人员仍然需要记住设置QueryTrackingBehavior.NoTracking
。最好在上下文类本身的构造函数中这样做,结果是,您将不再有跟踪查询的开销!
选择最佳装载策略
第九章讨论了实体框架核心可用于相关主数据或详细数据的加载策略(显式重新加载、快速加载和预加载)。不幸的是,我不能笼统地说什么是最好的装载策略。它总是取决于具体情况,而针对您的具体情况的最佳加载策略只能通过性能测试根据具体情况来确定。然而,一些总括声明仍然是可能的。
基本上,建议不要将连接的数据记录作为一个整体加载,如果它们不是绝对必要的,而是仅在实际需要时加载连接的数据集。它取决于潜在连接和已连接数据集的数量,以确定急切加载是否值得。
如果您知道需要连接的数据(例如,在数据导出的上下文中),您应该选择立即加载或预加载。与使用Include()
的急切加载相比,所示的预加载技巧在许多情况下可以显著提高性能。
如果您不确切知道是否需要链接的数据,那么在延迟加载和急切加载之间的选择通常是在瘟疫和霍乱之间的选择。通过额外的服务器往返,重新加载减慢了所有的速度,但是急切的加载减慢了更大的结果集的速度。然而,在大多数情况下,往返次数的增加比更大的结果集更不利于性能。
如果您不确定,不要将延迟加载或急切加载绑定到代码中,而是允许在运行时通过配置来控制它。因此,应用的操作者可以随着数据量的增加并根据应用的典型用户行为来调整应用。
贮藏
web 和桌面应用都有经常使用但很少在数据存储中更新的数据。在这些情况下,在 RAM 中基于时间的数据缓存是有用的。经典。NET 从 4.0 版本开始就有了组件System.Runtime.Caching.dll
。一个System.Runtime.Caching
的前兆已经出现了。NET 1.0 在 ASP.NET 的名称空间System.WebCaching
中System.Web.dll
。组件System.Runtime.Caching.dll
引进于。另一方面,NET 4.0 可以在所有类型的应用中使用。System.Runtime.Caching
本质上只提供一种缓存:MemoryCache
用于 RAM 缓存。通过从基类ObjectCache
派生,您可以开发其他缓存方法(例如,在专用缓存服务器上或在文件系统中)。Windows Server 的AppFabric
的缓存特性是另一个缓存选项,但它不是基于System.Runtime.Caching
的。
Note
。NET Core 用 NuGet 包Microsoft.Extensions.Caching.Memory
代替System.Runtime.Caching
。但是,System.Runtime.Caching
现在是的 Windows 兼容包的一部分。净芯( https://blogs.msdn.microsoft.com/dotnet/2017/11/16/announcing-the-windows-compatibility-pack
-for-net-core
),也可用于。NET 核心。
超高速缓冲存储系统
清单 17-17 展示了一个结合实体框架核心使用MemoryCache
的例子。首先,GetFlights1()
检查出发航班的列表是否已经在缓存中。如果列表不存在,所有相关的航班都将加载一个新的实体框架上下文实例。对于这个数据集,GetFlights1()
创建了一个名为FlightSet
的缓存条目。带有policy.AbsoluteExpiration = DateTime.Now.AddSeconds (5)
的程序代码确定缓存条目应该在五秒钟后过期。然后它会自动从 RAM 中消失。
当然,也可以为每个出发地点创建一个单独的缓存条目,在一个条目中缓存所有航班,然后从 RAM 中过滤它们。那么数据库访问的次数将会更少,但是 RAM 中也会有您可能不需要的数据。只有当数据量不太大时,才能考虑这一点。可以在应用配置文件(app.config
/ web.config
)中设置多少 RAM 用于缓存,可以是绝对兆字节(cacheMemoryLimitMegabytes
)或物理内存的百分比(physicalMemoryLimitPercentage
)。也可以定义这些限值(pollingInterval
)的检查间隔。作为通过应用配置文件定义这些参数的替代方法,可以将它们作为NameValueCollection
传递给MemoryCache
的构造函数。
清单 17-17 中的Demo_MemoryCache()
方法通过在 15 秒内每秒调用两次来测试GetFlights1()
的操作。清单 17-17 显示缓存解决方案按预期工作,每五秒钟重新加载一次航班。图 17-14 显示了输出。
图 17-14
Output to Listing 17-17
public static void Demo_MemoryCache()
{
CUI.MainHeadline(nameof(Demo_MemoryCache));
DateTime Start = DateTime.Now;
do
{
var flightSet = GetFlight1("Rome");
// you can process the flights here...
Console.WriteLine("Processing " + flightSet.Count + " flights...");
System.Threading.Thread.Sleep(500);
} while ((DateTime.Now - Start).TotalSeconds < 60);
CUI.Print("done!");
}
/// <summary>
/// GetFlight with MemoryCache (5 sek)
/// </summary>
private static List<Flight> GetFlight1(string departure)
{
string cacheItemName = "FlightSet_" + departure;
// Access to the cache entry
System.Runtime.Caching.MemoryCache cache = System.Runtime.Caching.MemoryCache.Default;
List<Flight> flightSet = cache[cacheItemName] as List<Flight>;
if (flightSet == null) // Element is NOT in the cache
{
CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache missed", ConsoleColor.Red);
using (var ctx = new WWWingsContext())
{
ctx.Log();
// Load flights
flightSet = ctx.FlightSet.Where(x => x.Departure == departure).ToList();
}
// Store flights in cache
CacheItemPolicy policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTime.Now.AddSeconds(5);
//or: policy.SlidingExpiration = new TimeSpan(0,0,0,5);
cache.Set(cacheItemName, flightSet, policy);
}
else // Data is already in cache
{
CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache hit", ConsoleColor.Green);
}
return flightSet;
}
Listing 17-17Timed Caching of Data Loaded with Entity Framework Using MemoryCache
Note
顺便说一下,System.Runtime.
Caching
可以做得更多,尤其是所谓的基于资源变化的缓存失效。例如,如果文件发生变化(HostFileChangeMonitor
)或数据库表的内容发生变化(SqlChangeMonitor
),可以立即删除缓存条目(甚至在设置的缓存期到期之前)。
缓存管理器
像GetFlights1()
这样的数据访问方法在每个应用中出现上百次甚至上千次。重复出现相同的程序代码来检查缓存条目的存在,并可能创建新的条目,这当然不是一个好的解决方案。
在清单 17-18 中,任务更加简洁和整洁。GetFlights2()
只包括调用CacheManager
类实例的通用Get()
方法。CacheManager
在实例化期间接收以秒为单位的缓存持续时间。除了描述返回类型的类型参数之外,Get()
方法还需要缓存条目名和对数据加载方法的引用。第三个和任何后续参数通过Get()
传递给 load 方法。load 方法GetFlights2Internal()
完全脱离了缓存方面,只负责用实体框架加载数据。也可以直接调用它,但这通常是不可取的。所以,这里也是“私”的。
public static void Demo_CacheManager()
{
CUI.MainHeadline(nameof(Demo_CacheManager));
DateTime Start = DateTime.Now;
do
{
var flightSet = GetFlight2("Rome");
// you can process the flights here...
Console.WriteLine("Processing " + flightSet.Count + " flights...");
System.Threading.Thread.Sleep(500);
} while ((DateTime.Now - Start).TotalSeconds < 60);
}
/// <summary>
/// GetFlight with CacheManager (5 sek)
/// </summary>
private static List<Flight> GetFlight2(string departure)
{
string cacheItemName = "FlightSet_" + departure;
var cm = new CacheManager<List<Flight>>(5);
cm.CacheHitEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache hit: " + text, ConsoleColor.Green); };
cm.CacheMissEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache missed: " + text, ConsoleColor.Red); };
return cm.Get(cacheItemName, GetFlight2Internal, departure);
}
private static List<Flight> GetFlight2Internal(object[] param)
{
using (var ctx = new WWWingsContext())
{
ctx.Log();
string departure = param[0] as string;
// Load flights
return ctx.FlightSet.Where(x => x.Departure == departure).ToList();
}
}
Listing 17-18Simplified Implementation of the Task Now with the CacheManager
然而,这个优雅的CacheManager
类不是. NET Framework 类,而是一个自实现。这个类的完整源代码如清单 17-20 所示。除了清单 17-18 中使用的通用Get()
方法,该方法需要一个类型参数和一个加载方法,您还可以使用另一个Get()
重载直接从缓存中检索数据。如果数据不存在,你在这里得到零。用Save()
也可以直接保存。CacheManager
类的用户看不到底层库System.Runtime.Caching
的任何东西。
就其本质而言,对于给定的任务,使用属性SlidingExpiration
而不是AbsoluteExpiration
听起来很诱人。然而,策略leads.SlidingExpiration = new TimeSpan (0,0,0,5)
说数据在第一次加载后将永远不会被重新加载,因为TimeSpan
(0,0,0,5)设置的 5 秒时间跨度指的是SlidingExpiration
。最后一次访问,即在最后一次读取访问后仅五秒钟,高速缓存条目被移除。要强制重新加载,您必须在方法Demo_CacheManager()
中将Sleep()
的持续时间设置为 5000 或更高。
如果你想更简洁一点,你应该看看清单 17-20 ,它显示了一个带有匿名函数的变体。不再需要编写单独的加载方法;必要的代码完全嵌入在GetFlights4()
中。得益于Closure
技术,Get()
不再需要获取Departure
作为参数,因为嵌入在GetFlights3()
中的匿名方法可以直接访问GetFlights3()
方法的所有变量。
public static void Demo_CacheManagerLambda()
{
CUI.MainHeadline(nameof(Demo_CacheManagerLambda));
DateTime Start = DateTime.Now;
do
{
var flightSet = GetFlight3("Rome");
// you can process the flights here...
Console.WriteLine("Processing " + flightSet.Count + " flights...");
System.Threading.Thread.Sleep(500);
} while ((DateTime.Now - Start).TotalSeconds < 60);
}
public static List<Flight> GetFlight3(string departure)
{
string cacheItemName = "FlightSet_" + departure;
Func<string[], List<Flight>> getData = (a) =>
{
using (var ctx = new WWWingsContext())
{
// Load flights
return ctx.FlightSet.Where(x => x.Departure == departure).ToList();
}
};
var cm = new CacheManager<List<Flight>>(5);
cm.CacheHitEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache Hit: " + text, ConsoleColor.Green); };
cm.CacheMissEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache Miss: " + text, ConsoleColor.Red); };
return cm.Get(cacheItemName, getData);
}
Listing 17-19Variant for Using CacheManager with an Anonymous Function
using System;
using System.Collections.Generic;
using System.Runtime.Caching;
namespace ITVisions.Caching
{
/// <summary>
/// CacheManager for simplified caching with System.Runtime.Caching
/// (C) Dr. Holger Schwichtenberg 2013-2017
/// </summary>
public class CacheManager
{
public static List<MemoryCache> AllCaches = new List<MemoryCache>();
public static bool IsDebug = false;
/// <summary>
/// Default cache duration
/// </summary>
public static int DefaultCacheSeconds = 60 * 60; // 60 minutes
/// <summary>
/// Reduced cache duration in debug mode
/// </summary>
public static int DefaultCacheSeconds_DEBUG = 10; // 10 seconds
/// <summary>
/// Removes all entries from all caches
/// </summary>
public static void Clear()
{
MemoryCache.Default.Dispose();
foreach (var c in AllCaches)
{
c.Dispose();
}
}
/// <summary>
/// Removes all entries with name part from all caches
/// </summary>
/// <param name="name"></param>
public static void RemoveLike(string namepart)
{
foreach (var x in MemoryCache.Default)
{
if (x.Key.Contains(namepart)) MemoryCache.Default.Remove(x.Key);
}
foreach (var c in AllCaches)
{
foreach (var x in MemoryCache.Default)
{
if (x.Key.Contains(namepart)) MemoryCache.Default.Remove(x.Key);
}
}
}
}
/// <summary>
/// CacheManager for simplified caching with System.Runtime.Caching
/// (C) Dr. Holger Schwichtenberg 2013-2017
/// </summary>
/// <typeparam name="T">type of cached data</typeparam>
/// <example>
/// public List<Datentyp> GetAll()
/// {
/// var cm = new CacheManager<List<Datentyp>>();
/// return cm.Get("Name", GetAllInternal, "parameter");
/// }
/// public List<Datentyp> GetAllInternal(string[] value)
/// {
/// var q = (from x in Context.MyDbSet where x.Name == value select x);
/// return q.ToList();
/// }
/// </example>
public class CacheManager<T> where T : class
{
/// <summary>
/// CacheHit or CassMiss
/// </summary>
public event Action<string> CacheEvent;
/// <summary>
/// triggered when requested data is in the cache
/// </summary>
public event Action<string> CacheHitEvent;
/// <summary>
/// triggered when requested data is not in the cache
/// </summary>
public event Action<string> CacheMissEvent;
private readonly int _seconds = CacheManager.DefaultCacheSeconds;
public MemoryCache Cache { get; set; } = MemoryCache.Default;
/// <summary>
/// Created CacheManager with MemoryCache.Default
/// </summary>
public CacheManager()
{
if (CacheManager.IsDebug || System.Diagnostics.Debugger.IsAttached)
{
this._seconds = CacheManager.DefaultCacheSeconds_DEBUG;
}
else
{
this._seconds = CacheManager.DefaultCacheSeconds;
}
}
public CacheManager(int seconds) : this()
{
this._seconds = seconds;
}
/// <summary>
/// Generated CacheManager with its own MemoryCache instance
/// </summary>
/// <param name="seconds">Gets or sets the maximum memory size, in megabytes, that an instance of a MemoryCache object can grow to.</param>
/// <param name="cacheMemoryLimitMegabytes"></param>
/// <param name="physicalMemoryLimitPercentage">Gets or sets the percentage of memory that can be used by the cache.</param>
/// <param name="pollingInterval">Gets or sets a value that indicates the time interval after which the cache implementation compares the current memory load against the absolute and percentage-based memory limits that are set for the cache instance.</param>
public CacheManager(int seconds, int cacheMemoryLimitMegabytes, int physicalMemoryLimitPercentage, TimeSpan pollingInterval)
{
var config = new System.Collections.Specialized.NameValueCollection();
config.Add("CacheMemoryLimitMegabytes", cacheMemoryLimitMegabytes.ToString());
config.Add("PhysicalMemoryLimitPercentage", physicalMemoryLimitPercentage.ToString());
config.Add("PollingInterval", pollingInterval.ToString());
Cache = new MemoryCache("CustomMemoryCache_" + Guid.NewGuid().ToString(), config);
Console.WriteLine(Cache.PhysicalMemoryLimit);
Console.WriteLine(Cache.DefaultCacheCapabilities);
this._seconds = seconds;
}
/// <summary>
/// Get element from cache. It will not load if it is not there!
/// </summary>
public T Get(string name)
{
object objAlt = Cache[name];
return objAlt as T;
}
/// <summary>
/// Get element from cache or data source. Name becomes the name of the generic type
/// </summary>
public T Get(Func<string[], T> loadDataCallback, params string[] args)
{
return Get(typeof(T).FullName, loadDataCallback, args);
}
/// <summary>
/// Retrieves item from cache or data source using the load method.
/// </summary>
public T Get(string name, Func<string[], T> loadDataCallback, params string[] args)
{
string cacheInfo = name + " (" + Cache.GetCount() + " elements in cache. Duration: " + _seconds + "sec)";
string action = "";
object obj = Cache.Get(name);
if (obj == null) // not in cache
{
action = "Cache miss";
CacheMissEvent?.Invoke(cacheInfo);
CUI.PrintVerboseWarning(action + ": " + cacheInfo);
#region DiagnoseTemp
string s = DateTime.Now + "################ CACHE MISS for: " + cacheInfo + ": " + loadDataCallback.ToString() + System.Environment.NewLine;
int a = 0;
var x = Cache.DefaultCacheCapabilities;
foreach (var c in Cache)
{
a++;
s += $"{a:00}: LIMIT: {Cache.PhysicalMemoryLimit}:" + c.Key + ": " + c.Value.ToString().Truncate(100) + System.Environment.NewLine;
}
Console.WriteLine(s);
#endregion
// load data now
obj = loadDataCallback(args);
// and store it in cache
Save(name, obj as T);
}
else // found in cache
{
action = "Cache hit";
CUI.PrintVerboseSuccess(action + ": " + cacheInfo);
CacheHitEvent?.Invoke(cacheInfo);
}
// return data
CacheEvent?.Invoke(action + " for " + cacheInfo);
return obj as T;
}
/// <summary>
/// Saves an object in the cache
/// </summary>
public void Save(string name, T obj)
{
if (obj == null) return;
object objAlt = Cache[name];
if (objAlt == null)
{
CacheItemPolicy policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTime.Now.AddSeconds(_seconds);
policy.RemovedCallback = new CacheEntryRemovedCallback(this.RemovedCallback);
Cache.Set(name, obj, policy);
}
}
public void RemovedCallback(CacheEntryRemovedArguments arguments)
{
}
/// <summary>
/// Removes an entry with specific names from this cache
/// </summary>
/// <param name="name"></param>
public void Remove(string name)
{
if (Cache.Contains(name)) Cache.Remove(name);
}
/// <summary>
/// Removes all entries with specific name part from this cache
/// </summary>
public void RemoveLike(string namepart)
{
foreach (var x in Cache)
{
if (x.Key.Contains(namepart)) Cache.Remove(x.Key);
}
}
}
}
Listing 17-20The Auxiliary Class CacheManager Simplifies the Use of System.Runtime.Caching
使用 EFPlus 的二级缓存
清单 17-18 中显示的CacheManager
是一个通用的解决方案,它不仅允许你缓存实体框架对象,还允许你缓存任何形式的数据。实体框架核心上的缓存甚至可以更优雅!对于实体框架核心,在附加库实体框架 Plus (EFPlus)和EFSecondLevelCache.Core
中有一个特殊的缓存解决方案(参见第二十章)。
这些组件基于System.MemoryCache.Runtime.Caching
实现了一个上下文无关的查询结果缓存。这种缓存称为二级缓存。这些额外的组件可以操作查询,以便实体框架核心将其结果具体化为对象,并将其不仅存储在上下文实例的一级缓存中,还存储在流程级别的二级缓存中。然后,另一个上下文实例可以在这个二级缓存中查找相同的查询,并传递存储在那里的对象,而不是来自数据库管理系统的新查询(参见图 17-15 )。
图 17-15
How a second-level cache works Note
本节讨论EFPlus. EFSecondLevelCache.Core
中的二级缓存在配置上更复杂,但也更灵活,因为除了主内存缓存(MemoryCache
),Redis 也可以作为缓存。
设置二级缓存
对于 EFPlus,不需要在上下文类中设置二级缓存。
使用二级缓存
清单 17-21 展示了二级缓存在 EFPlus 中的应用。在GetFlights4()
中,在 LINQ 查询中使用了FromCache()
方法,以 NuGet 包Microsoft.Extensions.Caching.Abstraction
中类型为MemoryCacheEntryOptions
的对象的形式指定缓存持续时间(这里:五秒)。
或者,您可以集中设置缓存持续时间,然后省略FromCache()
参数。
var options = new MemoryCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddSeconds(5) };
QueryCacheManager.DefaultMemoryCacheEntryOptions = options;
注意GetFlights4()
每次被调用时都会创建一个新的上下文实例,但是缓存仍然有效,如图 17-16 所示。
图 17-16
Output of Listing 17-21
不幸的是,与直接使用MemoryCache
对象的解决方案不同,您无法获得对象是否来自缓存或何时查询数据库的信息,因为不幸的是,在这种情况下,EFPlus 的缓存管理器不会触发任何结果。因此,您可以通过实体框架日志记录(ctx.Log()
)从数据库访问中获得缓存行为;参见第十二章)或通过外部探查器(例如,实体框架探查器或 SQL Server 探查器)。
public static void Demo_SecondLevelCache()
{
CUI.MainHeadline(nameof(Demo_SecondLevelCache));
DateTime Start = DateTime.Now;
do
{
var flightSet = GetFlight4("Rome");
// you can process the flights here...
Console.WriteLine("Processing " + flightSet.Count + " flights...");
System.Threading.Thread.Sleep(500);
} while ((DateTime.Now - Start).TotalSeconds < 30);
GetFlight4("Rome");
GetFlight4("Rome");
GetFlight4("Rome");
GetFlight4("Paris");
GetFlight4("Mailand");
GetFlight4("Mailand");
GetFlight4("Rome");
GetFlight4("Paris");
}
/// <summary>
/// Caching with EFPlus FromCache() / 5 seconds
/// </summary>
/// <param name="departure"></param>
/// <returns></returns>
public static List<Flight> GetFlight4(string departure)
{
using (var ctx = new WWWingsContext())
{
ctx.Log();
var options = new MemoryCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddSeconds(5) };
// optional: QueryCacheManager.DefaultMemoryCacheEntryOptions = options;
Console.WriteLine("Load flights from " + departure + "...");
var flightSet = ctx.FlightSet.Where(x => x.Departure == departure).FromCache(options).ToList();
Console.WriteLine(flightSet.Count + " Flights im RAM!");
return flightSet;
}
}
Listing 17-21Second-Level Caching with EFPlus
十八、带有实体框架核心的软件架构
实体框架核心无疑属于数据访问层。但是使用实体框架核心时,层模型整体看起来是什么样的呢?在这一章中,我简要地讨论了几种可供选择的架构。
整体模型
实体框架核心可以在一个单一的软件模型中使用。换句话说,实体框架核心上下文的实例化和命令(LINQ、存储过程、SQL)的执行都在表示层(见图 18-1 )。然而,这仅在非常小的应用中有意义(参见附录 A 中的 app MiracleList Light)。
图 18-1
Entity Framework Core in the monolithic software architecture model
作为数据访问层的实体框架核心
图 18-2 左侧显示了多层应用的一般结构,右侧显示了一个简单的多层软件架构模型,使用实体框架核心进行数据访问。这种实用的软件架构模型不需要专用的数据访问层。相反,实体框架上下文是完整的数据层。覆盖层是业务逻辑层,它通过语言集成查询(LINQ)命令和存储过程的调用(包括所需的直接 SQL 命令)来控制数据访问。根据业务逻辑层中的语句,实体框架上下文填充实体类。实体类通过所有层向下传递到表示层。
图 18-2
The pragmatic Entity Framework Core–based software architecture model
一些软件架构师批评这种简化的模型,因为业务逻辑层被数据访问命令污染了。业务逻辑层实际上不应该包含任何数据库访问命令。如果你真的把 LINQ 等同于 SQL,你就能看到这一点。但是你也可以把 LINQ 理解为 SQL 的真正抽象。毕竟,LINQ 命令只是一系列与数据库无关的方法调用;C# 和 Visual Basic 中类似 SQL 的语法对于软件开发人员来说只是一种语法糖。C# 或 Visual Basic 编译器立即使 LINQ 命令再次成为方法调用字符串。你也可以用这个方法调用字符串本身,也就是用collection.Where(x => x.CatID > 4).OrderBy(x => x.Name)
代替x in
collection where x.CatID > 4 orderby x.Name
。但是方法调用正是业务逻辑和数据访问控制通常相互通信的形式;也就是说,除了业务逻辑和数据层之间的常见做法之外,业务逻辑层中 LINQ 的必要使用没有任何作用。LINQ 只是比数据层的大多数 API 更通用。
实际上,业务逻辑层的一些污染是业务逻辑层中实体框架上下文实例的使用。这意味着业务逻辑层必须有一个对实体框架核心组件的引用。对象关系映射器的后续替换意味着业务逻辑层的变化。但是这种模式的明显优势是简单。您不必编写自己的数据库访问层,这样可以节省时间和金钱。
纯商业逻辑
然而,一些软件架构师会拒绝前面的实用模型,因为它太简单了,而是依赖于第二个模型(见图 18-3 )。这样,您就创建了自己的数据访问控制层。在这个数据访问控制层中,所有的 LINQ 调用和存储过程包装器方法都被再次打包在自己编写的方法中。这些方法然后调用业务逻辑层。在该模型中,只有数据访问控制层需要对实体框架核心组件的引用;因此,业务逻辑保持“纯净”
图 18-3
The textbook Entity Framework Core–based software architecture model without distribution
第二个软件架构模型对应于“纯”原则,但是在实践中,它也需要更多的实现工作。特别是在严格意义上几乎没有业务逻辑的“数据之上的表单”应用的情况下,开发人员必须实现许多“烦人的”包装例程。对于 LINQ,数据库访问层的GetCustomers()
包含 LINQ 命令,业务逻辑中的GetCustomers()
转发到数据库访问层的GetCustomers()
。使用存储过程时,两层都只传递。
业务类和视图模型类
这里要讨论的第三个软件架构模型(见图 18-4 )甚至更进一步,进行了一个抽象步骤,并且还禁止将实体类传递给所有层。相反,会发生实体类到其他类的映射。这些其他类通常被称为业务(对象)类,有时也被称为数据传输对象(dto ),与数据密集型实体类相反。在模型 3b 中(图的右侧),这些业务对象类再次被映射到作为模型-视图-视图模型(MVVM)模式的一部分为视图专门格式化的类。
图 18-4
Business objects and ViewModel classes
如果创建的实体类与实体框架核心(类似于经典 ADO.NET 实体框架第一版中的EntityObject
基类)有关系,那么基于业务类的软件架构模型将是强制性的。但在实体框架核心中却不是这样。使用基于业务类的模型的一个很好的理由是,如果实体类的设计符合表示层的需求,例如,因为它是一个“历史发展”的数据库模式。
然而,这种基于业务类的模型意味着相当大的实现开销,因为所有数据都必须从实体类转移到业务类。当然,对于新的和改变的对象,这种转移必须以相反的方向实现。这种对象到对象的映射(OOM)不能与像 Entity Framework Core 这样的对象关系映射器一起工作。但是,也有其他用于对象到对象映射的框架,比如 AutoMapper ( http://automapper.org
)和 value injector(http://valueinjecter.codeplex.com
)。但是即使有这样的框架,实现工作也是很重要的,特别是因为没有用于对象到对象映射的图形化设计器。
此外,由于额外的映射需要计算时间,因此不仅在开发时,而且在运行时,工作量都更大。
分布式系统
图 18-5 、图 18-6 和图 18-7 展示了以实体框架为数据访问核心的分布式系统的六种软件架构模型。现在,客户端不能直接访问数据库,但是应用服务器上有一个服务外观,客户端中有代理类(调用服务外观)。在业务逻辑层和数据访问层的划分方面,您有与架构 1 和架构 2 相同的选择。这里没有显示这些选项。更多的是关于实体类。如果您在客户端和服务器端使用相同的类,这称为共享契约。每当服务器和客户机被写入时,这都是可能的。因此,客户端可以使用服务器中的类引用该程序集。共享合同的情况如图 18-5 左侧架构 4 所示;这里,客户端也使用实体框架核心实体类。
图 18-7
More Entity Framework Core–based software architecture models with distribution
图 18-6
Entity Framework Core–based software architecture models in a distribution system
图 18-5
Entity Framework Core–based software architecture models in a distribution system
如果客户端有不同的平台,那么您必须为实体类创建代理类。在图 18-5 所示的架构 5 中,显式代理类是期望的或必要的,因为客户端不是. NET。
架构模型 6 到 9 的区别仅在于实体类的映射。
- 尽管 architecture 6 使用共享契约,但它将实体类映射到为在线传输而优化的 DTO 类。在客户端,有另一个映射到 ViewModel 类的 OO。
- Architecture 7 假设代理类和到 ViewModel 类的 OO 映射。
- 架构 8 使用 DTO、代理和视图模型类。
- 最复杂的模型,architecture 9,也在客户端使用业务对象类。
你可能想知道谁使用 architecture 9。事实上,在我作为顾问的工作中,我看到许多软件架构都是这样精心设计的。这些是大型团队参与的项目,然而每个小的用户请求都需要很长的实现时间。
结论
软件架构师在使用实体框架核心时有很多架构选择。带宽从一个简单、实用的模型开始(有一些妥协),其中开发人员只需要实现三个程序集。在这里展示的架构模型的另一边,您至少需要 12 个组件。
选择哪种架构模型取决于各种因素。当然,这包括具体的需求、系统环境和可用软件开发人员的专业知识。而且预算也是一个重要因素。在我作为公司顾问的日常生活中,我一次又一次地体验到,软件架构师选择太复杂的架构,因为“纯”的原则不适应业务条件。在这样的系统中,即使是最小的用户请求(“我们仍然需要左边的字段”)通常也是非常耗时和昂贵的。许多项目因为不必要的复杂软件架构而失败。
Tip
使用尽可能少的层。在向软件架构模型添加另一个抽象之前,请三思。
十九、商业工具
本章介绍了可用于实体框架核心的商业工具。我绝不参与这些工具的开发或分发。
实体框架核心动力工具
微软为经典的实体框架提供了强大的工具,但是实体框架核心的重新发布现在由外部开发者实现。实体框架 Core Power Tools 是 Visual Studio 2017 的免费扩展。
| | ![A461790_1_En_19_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_19_Figa_HTML.jpg) | | 工具名称 | 实体框架核心动力工具 | | 网站 | [`https://www.visualstudiogallery.msdn.microsoft.com/9674e1bb-d942-446a-9059-a8b4bd18dde2`](https://www.visualstudiogallery.msdn.microsoft.com/9674e1bb-d942-446a-9059-a8b4bd18dde2) | | 制造商 | 埃里克·埃斯基科夫·詹森(MVP), [`https://github.com/ErikEJ`](https://github.com/ErikEJ) | | 免费版本 | 是 | | 商业版 | 不 |特征
安装后,可以通过 Visual Studio 解决方案资源管理器中项目的上下文菜单来访问实体框架核心功能工具(图 19-1 )。该附件提供以下功能:
图 19-1
Entity Framework Core Power Tools in the context menu of a project in Solution Explorer
- 用于对 SQL Server、SQL Compact 和 SQLite 中的现有数据库进行反向工程的图形用户界面
- 为给定的实体框架核心上下文创建图表的能力
- 为数据库模式创建图表的能力
- 显示 SQL DDL 命令的能力,用于为实体框架核心上下文及其实体类创建数据库模式
使用实体框架核心工具进行逆向工程
实体框架核心动力工具逆向工程由三个步骤组成。第一步,通过 Visual Studio 的标准对话框选择数据库(图 19-2 )。在第二步中,您选择桌子(图 19-3 )。您可以将表格选择保存为文本文件,并为新的调用重新加载(图 19-4 )。在第三步中,您设置选项,这些选项也是Scaffold-DbContext
cmdlet 所允许的(图 19-5 )。之后,代码生成与Scaffold-DbContext
中的相同(图 19-6 )。
Note
就像使用Scaffold-DbContext
一样,在数据库改变后更新程序代码(从数据库更新模型)不是由 Power Tools 实现的。
图 19-2
Reverse engineering with Entity Framework Core Power Tools (step 1)
图 19-3
Reverse engineering with Entity Framework Core Power Tools (step 2)
图 19-4
Storage of the table selection in a text file
图 19-5
Reverse engineering with Entity Framework Core Power Tools (step 3)
图 19-6
Generated code with Entity Framework Core Power Tools reverse engineering
带有实体框架核心功能工具的图表
图 19-7 显示了实体框架核心模型的图形表示,作为一个由命令 Add DbContext Model Diagram 生成的有向图标记语言(DGML)文件。
图 19-7
Entity Framework Core model as a diagram with Entity Framework Core Power Tools
你也可以在运行时用扩展方法AsDgml()
生成这个图,这个扩展方法在 NuGet 包ErikEJ.EntityFrameworkCore.DgmlBuilder
中可用(清单 19-1 )。
using System;
namespace EFC_PowerTools
{
class Program
{
static void Main(string[] args)
{
using (var ctx = new Wwwingsv2_ENContext())
{
var path = System.IO.Path.GetTempFileName() + ".dgml";
System.IO.File.WriteAllText(path, ctx.AsDgml(), System.Text.Encoding.UTF8);
Console.WriteLine("file saved:" + path);
}
}
}
}
Listing 19-1Using AsDgml( )
linqpad
语言集成查询(LINQ)因其静态类型而受到开发人员的欢迎。但是总是不得不运行编译器来尝试 LINQ 命令是很烦人的。当您在 Microsoft SQL Server 管理中使用查询编辑器时,您输入一个 SQL 命令,按 F5 键(或单击“执行”),然后查看结果。微软曾经想过让 LINQ 的实体以同样的方式在 Management Studio 中运行实体框架,但至今没有任何成果。
第三方工具 LINQPad 允许 LINQ 命令的交互输入和在编辑器中的直接执行。您可以对 RAM 中的对象(对象的 LINQ)、实体框架/实体框架核心和各种其他 LINQ 提供者执行 LINQ 命令。
| | ![A461790_1_En_19_Figb_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_19_Figb_HTML.jpg) | | 工具名称 | linqpad | | 网站(全球资讯网的主机站) | [`www.linqpad.net`](http://www.linqpad.net) | | 制造商 | 约瑟夫·阿尔巴哈里,澳大利亚 | | 免费版本 | 是 | | 商业版 | 起价 45 美元 |LINQPad 是免费的免费软件版本。但是如果你想享受 Visual Studio 风格的智能感知输入支持,你必须购买专业版或高级版。在高级版本中,也有许多包含的程序代码片段。同样,在高级版本中,您可以使用几个数据库来定义查询。当前版本 5 的系统要求是。NET 框架 4.6。只有 5MB 的大小,这个应用是非常轻量级的。该工具的作者说,“当你安装它的时候,它不会减慢你的电脑速度!”
使用 LINQPad
LINQPad 在左上角显示一个连接窗口(见图 19-8 )。在下面,您可以从提供的示例集合中进行选择(简单地说,来自 C# 6.0 这本书)或者保存您自己的命令(在我的查询下)。在主区域你会发现编辑器在顶部,输出区域在底部(见图 19-8 的中间/右侧)。
LINQPad 支持 C#、Visual Basic 的语法。NET,和 F# 以及 SQL 和实体 SQL(后者只针对经典的实体框架)。
图 19-8
LINQPad in action with LINQ to Objects
包括数据源
要针对实体框架核心上下文运行 LINQ 命令,必须使用添加连接添加连接。然而,该对话框目前仅显示 LINQ 到 SQL 和经典实体框架的驱动程序。通过查看更多驱动程序,您可以下载实体框架核心的驱动程序(图 19-9 )。
图 19-9
Adding Entity Framework Core drivers for LINQPad
添加驱动程序后,您应该能够选择实体框架核心(图 19-10 )。
图 19-10
Selecting Entity Framework Core as the LINQPad driver
选择提供者之后,您必须集成一个实体框架核心上下文。为此,使用浏览(见图 19-11 )来选择一个实现这种上下文的. NET 程序集。
Note
LINQPad 本身不为实体框架和实体框架核心创建上下文类。您总是需要使用 Visual Studio 或其他工具来创建和编译这样的类。
图 19-11
An Entity Framework Core context class has been selected
合并上下文后,你可以在 LINQPad 的左边看到现有的实体类(图 19-12 )。
图 19-12
After incorporating the context class
执行 LINQ 命令
一些命令可以直接从实体类的上下文菜单中执行(见图 19-13 )。
图 19-13
Predefined commands in the context menu of the entity class
在查询区域,您甚至可以输入命令(在支持输入的商业版本中)。图 19-14 显示了一个带有条件、预测和急切加载的 LINQ 命令。在急切加载的情况下,结果视图是分层的。
图 19-14
Execution of a custom LINQ command in LINQPad
除了结果视图之外,还可以在输出区域的其它选项卡中以下列格式显示“LINQ 到实体”命令:
- lambda 语法中的 LINQ 命令
- SQL 形式的 LINQ 命令
- 微软中间语言(IL)中的 LINQ 命令
- LINQ 命令作为表达式树
节约
查询可以保存为文件扩展名为.
linq
的文本文件。
结果可以导出为 HTML、Word 和 Excel 格式。
其他 LINQPad 驱动程序
除了实体框架和实体框架核心驱动程序之外,LINQPad 还提供其他工具的驱动程序,如下所示:
- 开放数据协议(OData)源
- 关系数据库 Oracle、MySQL、SQLite、RavenDB
- 云服务微软 StreamInsight 和 Azure 表存储
- Windows 事件跟踪(ETW)
- ORM mappers Mindscape LightSpeed、LLBLGen Pro、DevExpress XPO、DevForce
- NoSQL 数据库文件数据库
交互式程序代码输入
除了运行 LINQ 命令之外,LINQPad 工具还可以执行任何其他 C#、F# 和 Visual Basic 命令。可以在语言下选择表达模式和陈述模式。表达式模式捕捉单个表达式,然后打印结果,如System.DateTime.Now.ToString(new System.Globalization.CultureInfo("ya-JP"))
所示。这些表达式应该以分号结束。一次只能做一个表情。如果编辑器中有多个表达式,必须首先标记要执行的表达式。
另一方面,在语句模式下,记录完整的程序代码片段,每个命令以分号结束。你用Console.WriteLine()
发出命令。清单 19-2 显示了一个小的测试程序。
<Query Kind="Statements" />
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
Listing 19-2Small Test Program for LINQPad
也定义你自己的类型(z.Classes
;见清单 19-3 是可能的。但是,请注意,LINQPad 将捕获的代码嵌入到自己的默认代码中。因此,以下规则适用:
- 要执行的主程序代码必须在顶部。
- 在下面的类型定义之前,它必须用一个额外的花括号括起来。
- 类型定义必须在末尾,并且最后一个类型定义不能有右花括号。
因此,在内部,LINQPad 显然用顶部的main()
和底部的花括号来补充类型定义。
<Query Kind="Statements" />
var e = new Result() { a = 1, b = 20 };
for (int i = 0; i < e.b; i++)
{
e.a += i;
Console.WriteLine(i + ";" + e.a);
}
} // This extra parenthesis is required!
// The type definition must be after the main program!
class Result
{
public int a { get; set; }
public int b { get; set; }
// // here you have to omit the parenthesis!
Listing 19-3Small Test Program for LINQPad with Class Definition
LINQPad 的结论
LINQPad 是一个非常有用的工具,可以用来学习 LINQ、测试 LINQ 命令,通常还可以用来测试 C#、Visual Basic 和 F# 中的命令,而无需启动像 Visual Studio 这样的重量级程序或在现有项目中安装一个变通例程。由于实用的导出功能,LINQPad 不仅可以用于开发,还可以在日常实践中用作专门的数据库查询工具。
实体开发者
微软还没有为实体框架核心提供 GUI 开发工具。DevArt 和产品实体开发人员已经弥合了这一差距。
在过去,DevArt 为经典的实体框架提供了比微软本身更多的工具功能。现在,它又以实体框架核心工具领先。Entity Developer 通过图形设计器支持实体框架核心中的反向工程和正向工程。
| | ![A461790_1_En_19_Figc_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_19_Figc_HTML.jpg) | | 工具名称 | 实体开发者 | | 网站 | [`www.devart.com/entitydeveloper`](http://www.devart.com/entitydeveloper) | | 制造商 | 捷克共和国德瓦特 | | 免费版本 | 是 | | 商业版 | 99.95 美元起 |图 19-15 显示了可用的产品变型。免费速成版最多可以管理十个表的模型。
图 19-15
Variants of Entity Developer
安装 Entity Developer 时,除了独立的 Entity Developer 应用之外,安装程序已经提供了与 Visual Studio 2015 和 Visual Studio 2017 的 VSIX 集成。这个应用非常小,需要大约 60MB 的磁盘空间。
选择 ORM 技术
根据安装的变体,Entity Developer 在启动时提供不同的 ORM 技术。对于实体框架核心,创建一个.efml
文件,对于经典实体框架,创建一个.edml
文件,对于 Telerik 数据访问,创建一个.daml
文件,对于 NHibernate,创建一个.hbml
文件。选择 EF 核心模型后(见图 19-16 ),向导的第二步是逆向工程(这里称为数据库优先)和正向工程(这里称为模型优先)之间的决策,如图 19-17 所示。除了微软 SQL Server 之外,Entity Developer 还支持 Oracle、MySQL、PostgreSQL、SQLite 和 IBMs DB2 作为数据库,每一个都结合了 DevArt 自己的实体框架核心驱动(参见 https://www.devart.com/dotconnect/#database
)。
Note
在 Visual Studio 中使用 Entity Developer 时,ORM 技术没有选择向导;取而代之的是 DevArt EF 核心模型、DevArt NHibernate 模型等特定的元素模板。
图 19-16
Selecting the ORM technique in Entity Developer
图 19-17
Entity Developer templates within Visual Studio
使用实体开发人员进行逆向工程
然后,数据库首先选择一个现有的数据库,并选择工件(表、视图、存储过程和表值函数),就像在 Visual Studio 的经典实体框架向导中一样,但好处是开发人员可以向下选择列级别(图 19-18 )。
接下来是代码生成命名约定向导中的一个页面,它远远超出了 Visual Studio 目前所提供的内容(图 19-19 )。在下面的选项页面中,一些选项(如 N:M 关系和每类型继承表)是灰色的,因为实体框架核心尚不具备这些映射能力(图 19-20 )。在倒数第二步中,您可以选择是将所有的工件都放在图表表面上,还是只将选定的工件放在表面上。也可以为每个模式名创建一个图表(图 19-21 )。对于每个图表,都会创建一个.view
文件。
在最后一步中,您选择代码生成模板。实体开发者提供直接应用多个代码生成模板(图 19-22 )。
图 19-22
Selecting diagram content
图 19-21
Selecting model properties
图 19-20
Many settings for the naming conventions of classes and class members in the code to be generated in Entity Developer
图 19-19
Selection of artifacts down to the column level
图 19-18
Selection of the process model
图 19-23 显示了 Entity Developer 提供的模板,在 https://www.devart.com/entitydeveloper/templates.html
几乎没有记录。您必须弄清楚生成的代码是否符合您的需求。可以使用“复制到模型文件夹”功能将预定义的模板复制到您自己的应用文件夹中的模板文件中,然后在那里进行修改。这些模板类似于 Visual Studio 中使用的文本模板转换工具包(??)模板,但是它们不兼容。与 ?? 模板不同,DevArt 模板允许代码生成受属性网格中设置的参数的影响。例如,您可以为名为 EF Core 的选定模板指定以下内容:
- 您可以指定实体类和上下文类在不同文件夹中的着陆(这里您可以捕获相对或绝对路径)。
- 可能会生成分部类。
- 接口
INotifyPropertyChanging
和INotifyPropertyChanged
可以在实体类中实现。 - 您可以设置实体类接收 Windows Communication Foundation(WCF)的注释
[DataContract]
和[DataMember]
。 - 您可以设置实体类接收注释
[Serializable]
。 - 您可以覆盖实体类
Equals()
。 - 可以实现
IClonable
的实体类。
实体开发人员将图表名称、模板及其参数以及生成文件列表存储在一个.edps
文件中。
图 19-23
Selecting the code generation template
完成非常灵活的助手后,当在设计器中查看模型时,您可能会有点失望,至少如果您选择了数据库视图。实体开发人员抱怨没有主键。这是因为实体框架核心尚未设置为映射视图,并像表一样处理视图,这些视图总是需要有一个主键。您必须在属性窗口中为每个数据库视图手动设置一个。
即使继承是可能的,表之间的关系也在 Entity Developer 和 Entity Framework Core 中建模为关联。对于经典的实体框架,实体开发人员可以选择识别每种类型的表继承,但是实体框架核心还不支持按类型的表继承。
现在,您可以在模型浏览器中调整图表或创建新图表(参见图 19-24 的左侧)。你可以从数据库浏览器中直接拖放额外的表、视图、过程和函数到模型中(见图 19-24 的右边),而不必在 Visual Studio 中反复运行向导。与 Visual Studio 中的经典实体框架工具一样,Entity Developer 可以管理每个重叠实体模型的多个图表。你可以通过拖放来改变实体类中属性的顺序,而奇怪的是,用微软的工具来改变顺序只能通过繁琐的上下文菜单或键盘快捷键来实现。不同实体之间也可以拖放属性。Entity Developer 允许将颜色分配给模型中的实体,以实现更好的视觉分离。然后,这种颜色会应用到实体所在的所有图中。在图表表面上,您还可以随时添加注释。
程序代码生成由菜单项“车型➤生成代码”(按键 F7)触发。标准代码生成模板 EF Core 创建了以下内容:
- 上下文类
- 每个表和每个视图一个实体类
- 存储过程的每个返回类型的类
使用存储过程和表值函数的程序代码在 context 类中,它可能很长。有趣的是,实体开发人员并不依赖实体框架核心来实现,而是通过DataReader
拾取数据记录,并自己实现完整的映射(参见清单 19-4 )。毕竟,Entity Developer 认识到清单中显示的存储过程GetFlight
返回与表Flight
相同的结构,因此在返回类型中使用实体类Flight
。实体开发者或模板也可以通过扩展方法FromSql()
使用实体框架核心。DevArt 自己实现的优点是它也适用于不返回实体类型的存储过程。实体框架核心还不能做到这一点。在这些情况下,Entity Developer 为返回类型创建自己的类。
图 19-24
Graphical designer in Entity Developer
public List<Flight> GetFlight(System.Nullable<int> FlightNo)
{
List<Flight> result = new List<Flight>();
DbConnection connection = this.Database.GetDbConnection();
bool needClose = false;
if (connection.State != ConnectionState.Open)
{
connection.Open();
needClose = true;
}
try
{
using (DbCommand cmd = connection.CreateCommand())
{
if (this.Database.GetCommandTimeout().HasValue)
cmd.CommandTimeout = this.Database.GetCommandTimeout().Value;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = @"Operation.GetFlight";
DbParameter FlightNoParameter = cmd.CreateParameter();
FlightNoParameter.ParameterName = "FlightNo";
FlightNoParameter.Direction = ParameterDirection.Input;
if (FlightNo.HasValue)
{
FlightNoParameter.Value = FlightNo.Value;
}
else
{
FlightNoParameter.DbType = DbType.Int32;
FlightNoParameter.Size = -1;
FlightNoParameter.Value = DBNull.Value;
}
cmd.Parameters.Add(FlightNoParameter);
using (IDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
Flight row = new Flight();
if (!reader.IsDBNull(reader.GetOrdinal("FlightNo")))
row.FlightNo = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FlightNo")), typeof(int));
if (!reader.IsDBNull(reader.GetOrdinal("Timestamp")))
row.Timestamp = (byte[])Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Timestamp")), typeof(byte[]));
else
row.Timestamp = null;
if (!reader.IsDBNull(reader.GetOrdinal("Airline")))
row.Airline = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Airline")), typeof(string));
else
row.Airline = null;
if (!reader.IsDBNull(reader.GetOrdinal("Departure")))
row.Departure = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Departure")), typeof(string));
if (!reader.IsDBNull(reader.GetOrdinal("Destination")))
row.Destination = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Destination")), typeof(string));
if (!reader.IsDBNull(reader.GetOrdinal("FlightDate")))
row.FlightDate = (System.DateTime)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FlightDate")), typeof(System.DateTime));
if (!reader.IsDBNull(reader.GetOrdinal("NonSmokingFlight")))
row.NonSmokingFlight = (bool)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"NonSmokingFlight")), typeof(bool));
if (!reader.IsDBNull(reader.GetOrdinal("Seats")))
row.Seats = (short)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Seats")), typeof(short));
if (!reader.IsDBNull(reader.GetOrdinal("FreeSeats")))
row.FreeSeats = (short)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FreeSeats")), typeof(short));
else
row.FreeSeats = null;
if (!reader.IsDBNull(reader.GetOrdinal("Pilot_PersonID")))
row.PilotPersonID = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Pilot_PersonID")), typeof(int));
else
row.PilotPersonID = null;
if (!reader.IsDBNull(reader.GetOrdinal("Memo")))
row.Memo = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Memo")), typeof(string));
else
row.Memo = null;
if (!reader.IsDBNull(reader.GetOrdinal("Strikebound")))
row.Strikebound = (bool)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Strikebound")), typeof(bool));
else
row.Strikebound = null;
if (!reader.IsDBNull(reader.GetOrdinal("`Utilization `")))
row.Utilization = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"`Utilization `")), typeof(int));
else
row.Utilization = null;
result.Add(row);
}
}
}
}
finally
{
if (needClose)
connection.Close();
}
return result;
}
Listing 19-4Mapping for the Stored Procedure GetFlight( )
如果您尚未执行这些步骤,现在可以将生成的程序代码包含在 Visual Studio 项目中。使用的模板文件可以在 Entity Developer 中随时调整(参见模型资源管理器中的分支模板)。还可以在 Entity Developer 中编辑模板,包括 IntelliSense 输入支持。
或者,您也可以使用已安装的 Visual Studio 扩展。在 Visual Studio 中,您会在类别数据下的元素模板中找到像 DevArt EF Core Model 这样的新条目。选择其中之一将打开与独立应用相同的助手,并最终打开相同的设计器(包括模型浏览器和模板编辑器)。好处是生成的程序代码自动属于 Visual Studio 项目,其中还包含了.efml
文件。
如果数据库模式已更改,您可以使用菜单项“模型➤从数据库更新模型”来更新模型。您可以使用定义数据库类型的一般映射规则。工具➤选项➤服务器选项下的. NET 类型。
数据预览也很有帮助(在实体类的上下文菜单中选择检索数据);它包括导航到链接的数据记录和分层展开。您也可以直接从每个实体的上下文菜单中的图表或从数据库浏览器中的表或视图访问数据预览。
与实体开发人员一起进行正向工程设计
我现在将介绍实体开发人员的正向工程。选择 Model First 后,对话框 Model Properties 打开,因为既没有现有的数据库,也没有它的工件或任何命名约定可供选择。使用模型优先设置中的设置,您可以设置数据库模式生成的标准。
- 默认精度:对于十进制数,是逗号前的位数
- 默认小数位数:对于小数,是小数点后的位数
- 默认长度:对于字符串,最大字符数(空表示字符串不受限制)
在模型优先的情况下,向导的第三步也是最后一步是用于选择代码生成模板的对话框。
然后出现空的设计器界面,您可以使用模型资源管理器中的符号用类、枚举、关联和继承关系填充该界面。然后通过属性窗口配置它们(见图 19-25 )。例如,您可以设置主键,启用[ConcurrencyCheck]
注释,并指定一个属性为存在于数据库中但不存在于生成的实体类中的影子属性。实体框架核心中有一些不可用的选项,比如 N:M 映射,实体开发人员甚至没有在实体框架核心模型中提供。
图 19-25
Creation and configuration of new properties in Entity Developer
使用函数模型➤从模型更新数据库,您可以从中创建数据库模式。该向导要求目标数据库,该数据库必须已经存在。然后,向导会显示哪些架构更改将被传输到数据库,并提供不传输某些更改的选项。在最后一步中,您可以查看要执行的 SQL 脚本。实体开发者不使用实体框架核心的命令行模式迁移工具(dotnet ef
或 PowerShell cmdlets 相反,它使用自己的方法将现有模式与目标模式进行比较。但是,Entity Developer 也会尝试获取数据。使用重建数据库表选项,您可以使现有的表(包括它们的数据)消失。实体开发人员迁移中不存在名为__EFMigrationsHistory
的附加表。您可以在“模型➤设置➤同步➤数据库命名”下定义要生成的数据库模式中的命名约定。
在模型➤的“从模型生成数据库脚本”菜单中,您可以为要生成的模式生成 SQL 脚本,而无需引用特定的数据库。这样,您可以配置目标数据库管理系统和版本(例如,对于 SQL Server ),如 2000、2005、2008、2012、2014 和 Azure(但不是 SQL Server 2016)。
实体开发者用很多小事来支持。例如,先前指定的默认值“无名称”不仅输入到数据库模式中,还用于实体类的构造函数中(参见清单 19-5 ),它是通过模型➤生成的代码(F7)在逆向工程中生成的。在代码生成设置中,设置 inotifypropertychanging➤inotifypropertychanging 和 WCF 数据协定属性已激活。用类和属性“名字”填充的注释已经输入到实体开发人员设计器中。此外,在 Entity Developer 中,您可以捕获对实体类型和属性的任何注释。为此,首先在上下文菜单中选择属性,然后选择。任何. NET 属性。NET 程序集。如果。NET 属性需要参数,您可以在对话框中捕获它们。您可以在属性窗口中设置一些注释,如[DisplayName]
、[Range]
、[RegularExpression]
(参见图 19-25 左下角的“验证”)。为了使验证注释在生成的程序代码中真正永久化,您必须在代码生成模板中选择一个验证框架。除了。NET 验证批注,您可以选择旧的。NET 企业库或 NHibernate 验证器。
有趣的是,您可以将属性网格扩展到任何设置。然后可以在代码生成过程中考虑这些设置。其他设置在“模型➤设置➤模型”下的树中定义,该树显示在“模型➤扩展属性”下,用于工件,如类、属性和关联。然后,您必须考虑这些附加设置在单独的.tmpl
代码生成模板中的意义。
//-------------------------------------------------------------------------
// This is auto-generated code.
//-------------------------------------------------------------------------
// This code was generated by Entity Developer tool using EF Core template.
// Code is generated on: 31/12/2017 00:04:31
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//-------------------------------------------------------------------------
using System;
using System.Data;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Data.Common;
using System.Collections.Generic;
namespace Model
{
public partial class Person {
public Person()
{
OnCreated();
}
public virtual string ID
{
get;
set;
}
public virtual string Name
{
get;
set;
}
public virtual System.DateTime Birthday
{
get;
set;
}
#region Extensibility Method Definitions
partial void OnCreated();
#endregion
}
}
//-------------------------------------------------------------------------
// This is auto-generated code.
//-------------------------------------------------------------------------
// This code was generated by Entity Developer tool using EF Core template.
// Code is generated on: 31/12/2017 00:04:31
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//-------------------------------------------------------------------------
using System;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.ComponentModel;
using System.Reflection;
using System.Data.Common;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Model
{
public partial class Model : DbContext
{
public Model() :
base()
{
OnCreated();
}
public Model(DbContextOptions<Model> options) :
base(options)
{
OnCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any(ext => !string.IsNullOrEmpty(ext.ConnectionString) || ext.Connection != null))
CustomizeConfiguration(ref optionsBuilder);
base.OnConfiguring(optionsBuilder);
}
partial void CustomizeConfiguration(ref DbContextOptionsBuilder optionsBuilder);
public virtual DbSet<Person> People
{
get;
set;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
this.PersonMapping(modelBuilder);
this.CustomizePersonMapping(modelBuilder);
RelationshipsMapping(modelBuilder);
CustomizeMapping(ref modelBuilder);
}
#region Person Mapping
private void PersonMapping(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().ToTable(@"People");
modelBuilder.Entity<Person>().Property<string>(x => x.ID).HasColumnName(@"ID").IsRequired().ValueGeneratedNever();
modelBuilder.Entity<Person>().Property<string>(x => x.Name).HasColumnName(@"Name").IsRequired().ValueGeneratedNever();
modelBuilder.Entity<Person>().Property<System.DateTime>(x => x.Birthday).HasColumnName(@"Birthday").HasColumnType(@"datetime2").IsRequired().ValueGeneratedNever();
modelBuilder.Entity<Person>().HasKey(@"ID");
}
partial void CustomizePersonMapping(ModelBuilder modelBuilder);
#endregion
private void RelationshipsMapping(ModelBuilder modelBuilder)
{
}
partial void CustomizeMapping(ref ModelBuilder modelBuilder);
public bool HasChanges()
{
return ChangeTracker.Entries().Any(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Added || e.State == Microsoft.EntityFrameworkCore.EntityState.Modified || e.State == Microsoft.EntityFrameworkCore.EntityState.Deleted);
}
partial void OnCreated();
}
}
Listing 19-5Example of an Entity Class Generated by Entity Developer
实体框架分析器
对象-关系映射意味着从 SQL 中抽象出来,自然会出现这样的问题:哪些命令以及有多少命令实际上被发送到数据库管理系统。您可以使用 DBMS 自己的探查器(如 Microsoft SQL Server 探查器)或特定于 ORM 的工具(如实体框架探查器)来监视通信。
几乎所有的 OR mappers 都使用自己的查询语言,比如 NHibernate 上的 HQL,Entity Framework 和 Entity Framework Core 上的 LINQ。这些语言在数据库无关的对象模型上工作,OR 映射器翻译成每个数据库管理系统的 SQL 方言。SQL 命令的自动生成总是对 ORM 的基本批评的起点,尤其是关于 SQL 优化器的仓库。事实上,并不是所有由 OR 映射器生成的 OR 语句都是最佳的。
使用 OR 映射器的软件开发人员的职责之一就是跟踪非优化的 SQL 和不利的加载策略。这就是冬眠犀牛公司的实体框架分析器的用武之地。它与实体框架核心和经典实体框架一起工作。
图 19-26
Entity Framework Profiler licensing options
| | ![A461790_1_En_19_Figd_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_19_Figd_HTML.jpg) | | 工具名称 | 实体框架分析器 | | 网站 | [`www.efprof.com`](http://www.efprof.com) | | 制造商 | 冬眠的犀牛,以色列 | | 免费版本 | 不 | | 商业版 | 每月 45 美元起 |集成实体框架分析器
为了使实体框架探查器能够记录 or 映射器和数据库管理系统之间的活动,必须对要监控的应用进行“检测”这需要对程序代码进行两处修改。
- 开发者必须有对
HibernatingRhinos
装配.Profiler.CreateAppender
.dll
的引用。
Tip
这个程序集附带了实体框架分析器(文件夹/Appender
)的三个版本:for。NET 3.5,对于。NET 4.x,并作为. NET 标准程序集(包括。网芯)。虽然您可以使用经典的。在. NET 核心项目中,你不应该直接从/Appender/netstandard/
文件夹中引用程序集。相反,你应该通过 NuGet ( Install-Package EntityFrameworkProfiler.Appender
)来安装它。否则,您可能会丢失依赖项。
- 在程序开始时(或者在程序中您希望开始分析的地方),程序行
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize()
出现在程序代码中。
Tip
实体框架探查器不要求应用在 Visual Studio 调试器中运行。如果应用是直接启动的,即使它是在发布模式下编译的,也可以进行记录。这是一行检测代码。因此,您可以创建应用,以便在需要时调用插装代码;例如,它可以由配置文件控制。
使用实体框架探查器监视命令
在要监控的应用之前,启动基于 WPF 的实体框架分析器用户界面(EFProf.exe
)。启动要监控的应用后,实体框架概要分析器(在左侧列表中)显示实体框架类ObjectContext
(或所有派生类)的所有已创建实例。不幸的是,单独的上下文实例没有命名;您必须自己在实体框架分析器用户界面中指定它们。
每个上下文包含通过上下文执行的 SQL 命令的数量以及相应的执行时间,包括 DBMS 中的执行时间和总时间,包括 RAM 中对象的具体化。例如,在图 19-27 中,问题出现了,许多对象上下文是在根本没有执行任何命令的情况下创建的。
Note
实体框架分析器谈到了对象上下文,在经典的实体框架中,对象上下文是实体框架上下文的原始基类。在实体框架核心中,只有更现代的DbContext
。实体框架探查器中的名称未更改。然而,实体框架分析器在实体框架和实体框架核心中都使用DbContext
基类。
图 19-27
Entity Framework Profiler in action
在屏幕的右边部分,您会发现当前所选上下文的已执行命令列表。在详细信息下,您可以看到完整的带参数的 SQL 命令和相关的执行计划(见图 19-28 )以及结果集。然而,要做到这一点,您必须在实体框架分析器中输入连接字符串(参见图 19-29 )。
Stack Trace 选项卡显示哪个方法触发了一个 SQL 命令。双击“堆栈跟踪”选项卡中的条目可以直接在打开的 Visual Studio 窗口中找到匹配的代码,这很好。这将帮助您快速找到触发 SQL 命令的 LINQ 或 SQL 命令。
图 19-29
Displaying the result in the Entity Framework Profiler
图 19-28
Execution plan of the database management system in the Entity Framework Profiler
警报和建议
特别注意灰色圆圈(建议)和红色圆圈(警告)(见图 19-30 )。这里,实体框架分析器帮助您发现潜在的问题。在图 19-30 中,这就是实体框架分析器所说的SELECT N + 1
问题。一个接一个地执行大量类似的 SQL 命令表明这里错误地使用了延迟加载。你应该考虑急于装货。
Entity Framework Profiler 很好地展示了另一个问题,即不推荐在不同的线程中使用上下文对象。其他提示(见图 19-30 )存在于查询使用许多连接,以通配符开始(如% xy
),返回许多记录,并且不包含TOP
语句(无界结果集)。最后一点是有争议的。这个提议的意图是,您不应该冒险要求比您实际预期需要的更多的记录。但是在实践中(除了使用滚动特性显式显示记录的应用),您通常无法设置永久适用的上限。当许多INSERT
、UPDATE
和DELETE
命令被执行时,也有一个警告,你应该检查这不是由批量操作映射的。
图 19-30
Alerts and suggestions in the Entity Framework Profiler
分析
分析选项卡中的分析功能非常有用。您会发现评估显示了以下内容:
图 19-31
Analysis of Queries By Method
- 程序代码中的哪些方法触发了哪些 SQL 命令(按方法查询;参见图 19-31
- 存在多少不同的命令(尽管名称为唯一查询,
INSERT
、UPDATE
和DELETE
命令也出现在这里!) - 哪些命令持续时间最长(昂贵的查询)
一个有趣的功能是隐藏在文件➤导出到文件菜单。这将创建一个 JavaScript 丰富的 HTML 页面,看起来像实体框架分析器。您可以查看所有的上下文实例和 SQL 命令,并调用分析结果。但是,缺少堆栈跟踪和警告。
正常存储功能创建一个二进制文件,文件扩展名为.efprof
。被监控的程序代码也可以通过调用 start 命令中的方法InitializeOfflineProfiling(filename.efprof)
而不是Initialize()
直接生成这样的文件。然后,在应用运行时,不必运行实体框架分析器 UI。因此,在目标系统上进行概要分析也是可能的,不会出现问题。
命令行支持和 API
就持续集成而言,您还可以从命令行运行实体框架分析器。但是每台计算机需要一个许可证。分析器本身在HibernatingRhinos.Profiler.Integration.dll
中也有一个编程接口。
实体框架分析器的结论
Entity Framework Profiler 是一个有用的工具,可以用来了解基于 Entity Framework 核心的应用实际执行哪些 SQL 命令。但是,价格高。
二十、附加组件
本章介绍了扩展实体框架核心功能的实体框架核心附加组件。我绝不参与这些工具的开发或分发。
DevArt 的 Oracle 数据库驱动程序
Oracle 目前不支持其数据库的实体框架核心。基本上,甲骨文公司已经表示,它正在努力支持( www.oracle.com/technetwork/topics/dotnet/tech-info/odpnet-dotnet-core-sod-3628981.pdf
),但到目前为止还没有解决方案。Oracle 花了几年时间为传统的实体框架提供解决方案。
作为 dotConnect for Oracle 产品的一部分,DevArt 提供了用于实体框架核心的商业 Oracle 驱动程序。
| | ![A461790_1_En_20_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_20_Figa_HTML.jpg) | | 组件名称 | Oracle 的 dotConnect | | 网站 | `https://www.devart.com/dotconnect/oracle/` | | 源代码 | [否](https://github.com/zzzprojects/EntityFramework-Plus) | | 纽吉特 | 设置:`dcoracleXYpro.exe` `Install-Package Devart.Data.Oracle.EFCore` | | 免费版本 | [否](https://github.com/zzzprojects/EntityFramework-Plus) | | 商业版 | $149.95 |装置
首先,应该在系统上执行 DevArt 安装包(dcoracleXYpro.exe
,其中XY
代表版本号)。另外,NuGet 包Devart.Data.Oracle.EFCore
应该安装在 context 类所在的项目中。
工具
Oracle 驱动程序使用标准实体框架核心工具进行逆向工程和正向工程。
Scaffold-DbContext "User ID=WWWings; Password=secret; Direct=true; Host=localhost; SID=ITVisions; Port=1521;" Devart.Data.Oracle.Entity.EFCore -Tables DEPT,EMP
Tip
或者,如果您想要一个用于 Oracle 数据库逆向工程或正向工程的图形用户界面,可以使用 DevArt 的 Entity Developer。
上下文类
在OnConfiguring()
中的 context 类中,可以用连接字符串调用方法UseOracle()
。
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseOracle(@"User ID=WWWings; Password=secret; Direct=true; Host=localhost; SID=ITVisions; Port=1521;");
}
Note
前面的连接字符串使用所谓的 Oracle 直接模式。这消除了对 Oracle 客户端设置的需求!如果您有一个类似于UserId = WWWings; Password = secret; Data Source = Name;
的连接字符串,您将需要为它安装 Oracle 客户端软件。否则,您将收到以下错误:“无法从注册表中获取 Oracle 客户端信息。请确保安装了 Oracle 客户端软件,并且应用(x86)的位与 Oracle 客户端的位相匹配,或者使用直接模式连接到服务器。有关数据源的信息,请参见https://docs.oracle.com/cd/B28359_01
/win.111/b28375/featConnecting.htm
。
实体类
请注意,Oracle 中的模式、表和列名每个只能包含 30 个字符( https://docs.oracle.com/database/121/SQLRF/sql_elements008.htm#SQLRF51129
)。如果名称太长,将出现以下运行时错误:“TableName ' entityclasswithlsupporteddatatypes '太长。指定了超过 30 个字符的标识符。
数据类型
图 20-1 和图 20-2 显示了 Oracle 列类型和。NET 数据类型。
图 20-2
Data type mapping during forward engineering (source: www.devart.com/dotconnect/oracle/docs/
)
图 20-1
Data type mapping during reverse engineering (source: www.devart.com/dotconnect/oracle/docs/
)
实体框架增强版
Entity Framework Plus (EFPlus)是传统实体框架的一个附加组件。尽管 EFPlus 网站只讨论实体框架,但是也有一个实体框架核心的变体。
Entity Framework Plus 为 Entity Framework Core 提供了几个附加功能:
| | ![A461790_1_En_20_Figb_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_20_Figb_HTML.jpg) | | 组件名称 | EFCore 的实体框架增强版 | | 网站 | [`http://entityframework-plus.net`](http://entityframework-plus.net) | | 源代码 | [`https://github.com/zzzprojects/EntityFramework-Plus`](https://github.com/zzzprojects/EntityFramework-Plus) | | 纽吉特 | `Install-Package Z.EntityFramework.Plus.EFCore` | | 免费版本 | 是 | | 商业版 | 不 |- 将
UPDATE
和DELETE
命令公式化为λ表达式(参见第十七章) - 审核(记录的所有更改都会自动记录在更改表中)
- 全局查询过滤器(实体框架核心从 2.0 版本开始就能做到这一点;EFPlus 也为 Entity Framework Core 1.x)提供了这一功能
- 作为
EFSecondLevelCache.Core
的替代方案的二级缓存(参见第十七章) - 查询批处理(通过数据库管理系统一次合并多个
SELECT
查询)
Note
EFPlus 1.6.11(及更新版本)支持实体框架核心版本 2.0。
使用 EFSecondLevelCache 进行二级缓存。核心
组件 EFSecondLevelCache。Core 为 Entity Framework Plus 中包含的二级缓存提供了替代的二级缓存。EFSecondLevelCache。Core 在配置上要复杂得多,但也更加灵活,因为除了主内存缓存(MemoryCache
),您还可以使用 Redis 作为缓存。
使用自动映射器的对象-对象映射
在现代软件架构中,典型的任务是关系数据库结构到对象的对象关系映射,以及不同对象结构之间的映射。开源工具 AutoMapper 促进了对象到对象的映射(OOM)。
将一种对象类型转换为另一种对象类型的要求是常见的,例如,在层之间的数据传输对象(dto)中,或者在包含用于显示或表达的渲染数据的视图模型中(见图 20-3 和图 20-4 )。要成像的物体类型通常相似,但不完全相同。并且它们通常没有允许在编程语言级别进行类型转换(即通过类型转换表达式)的公共基类或接口。
图 20-4
Object-to-object mapping is also used between entity classes and data transfer classes in modern software architectures
图 20-3
Object-to-object mapping is used between entity classes, business objects, and ViewModel classes
那个。NET 框架和。NET 核心类库不包含支持不同类型对象映射(对象到对象映射)的函数。中的类型转换器。NET 框架类库( http://msdn.microsoft.com/en-us/library/system.componentmodel.typeconverter.asp
)仅仅是定义了一个对象类型映射的公共接口。但是它对实际的成像工作没有帮助。
通过反射进行对象到对象的映射
手动编写对象到对象的映射意味着为迭代中的每个x
实例创建一个y
实例,并将x
的相关属性分别分配给y
的属性。对于可以在软件架构的更高层中改变的对象,您还会发现逆向程序代码,它将y
的属性映射回x
的属性。
编写这种对象-对象映射程序代码不是智力挑战,而是一项烦人的任务,很容易忘记属性。如果这样的映射任务是手工编程的,应用的维护工作总是会增加。毕竟,对于每个新的数据库字段,映射程序代码必须在应用中的许多不同点进行更改。
如果属性相同,并且具有相同的数据类型,那么您可以通过反射自己轻松地进行对象到对象的映射。清单 20-1 和清单 20-2 展示了遵循这个简单约定的类System.Object
的两个扩展方法。但是,如果名称不同(不规则)或者属性值没有 1:1 的映射,那么基本方法就没有用了。
using System;
using System.Reflection;
namespace EFC_Console.OOM
{
public static class ObjectExtensions
{
/// <summary>
/// Copy the properties and fields of the same name to another, new object
/// </summary>
public static T CopyTo<T>(this object from)
where T : new()
{
T to = new T();
return CopyTo<T>(from, to);
}
/// <summary>
/// Copy the properties and fields with the same name to another, existing object
/// </summary>
public static T CopyTo<T>(this object from, T to)
where T : new()
{
Type fromType = from.GetType();
Type toType = to.GetType();
// Copy fields
foreach (FieldInfo f in fromType.GetFields())
{
FieldInfo t = toType.GetField(f.Name);
if (t != null)
{
t.SetValue(to, f.GetValue(from));
}
}
// Copy properties
foreach (PropertyInfo f in fromType.GetProperties())
{
object[] Empty = new object[0];
PropertyInfo t = toType.GetProperty(f.Name);
if (t != null)
{
t.SetValue(to, f.GetValue(from, Empty), Empty);
}
}
return to;
}
}
}
Listing 20-1Copy of the Same Properties Between Two Classes via Reflection
using System;
using System.Linq;
using DA;
namespace EFC_Console.OOM
{
public class FlightDTO
{
public int FlightNo { get; set; }
public string Departure { get; set; }
public string Destination { get; set; }
public DateTime Date { get; set; }
}
public static class ReflectionMapping
{
public static void Run()
{
using (var ctx = new WWWingsContext())
{
var flightSet = ctx.FlightSet.Where(x => x.Departure == "Berlin").ToList();
foreach (var flight in flightSet)
{
var dto = flight.CopyTo<FlightDTO>();
Console.WriteLine(dto.FlightNo + ": " + dto.Departure +"->" + dto.Destination + ": " + dto.Date.ToShortDateString());
}
}
}
}
}
Listing 20-2Using the Extension Methods from Listing 20-1
自动驾驶
Jimmy Bogard 的开源库自动映射器已经在。面向对象映射的. NET 开发人员世界。NuGet 包自动映射器( https://www.nuget.org/packages/AutoMapper
)由AutoMapper.dll
组成,其中心类是AutoMapper.Mapper
。
AutoMapper 运行在以下基础上。净变量:
- 。网
- 。净核心
- 开发
- 。用于 Windows 应用商店应用/Windows 运行时的
- 通用 Windows 平台(UWP)应用
- 洗发精,快
- 巫师安卓系统
你可以在 GitHub 上找到源代码( https://github.com/AutoMapper/AutoMapper
)和一个 wiki ( https://github.com/AutoMapper/AutoMapper/wiki
)。您可以在 AutoMapper 网站( http://automapper.org
)上找到其他资源(例如,视频)。然而,总的来说,可用的文档(和许多开源项目一样)很少,不完整,有时还会过时。AutoMapper 的许多功能在 wiki 中没有描述,所以可以看看博客条目和论坛来全面了解 AutoMapper。到目前为止,还没有关于稳定版 3.3.1 和当前预发行版 4.0 之间差异的文档。即使是我,在我的项目中与 AutoMapper 一起工作了很长时间,也不得不花很多时间研究以发现软件组件中更多的、未记录的特性。
Note
本章描述了 6.1.1 版的 AutoMapper。不幸的是,AutoMapper 在过去有过重大变化,所以这里显示的命令在旧版本中只能部分工作。
看一个例子
在本例中,World Wide Wings 版本 2 对象模型的部分(如图 20-5 所示)将被映射到图 20-6 中的简化对象模型上。类别Pilot
、Employee
和Person
被解散。关于Pilot
的信息直接显示在FlightView
类中的一个字符串和一个名为PilotDetailView
的细节对象上。新的PassengerView
类还包括来自Person
类的个人数据。许多信息(例如,来自实体Employee
)在这里有意不再使用。
图 20-6
The simplified target model to be created from the model from Figure 20-5 using object-to-object mapping
图 20-5
The World Wide Wings version 2 object model that uses Entity Framework Core
清单 20-3 展示了这三个类。
using System;
using System.Collections.Generic;
namespace EFC_Console.ViewModels
{
public class FlightView
{
public int FlightNo { get; set; }
public string Departure { get; set; }
public string Destination { get; set; }
public string Date { get; set; }
public bool NonSmokingFlight { get; set; }
public short Seats { get; set; }
public Nullable<short> FreeSeats { get; set; }
public Nullable<int> FlightUtilization { get; set; }
public bool? BookedUp { get; set; }
public string SmokerInfo { get; set; }
public string Memo { get; set; }
public Nullable<bool> Strikebound { get; set; }
public byte[] Timestamp { get; set; }
public string PilotSurname { get; set; }
public string AircraftTypeDetailLength { get; set; }
public override string ToString()
{
return "Flight " + this.FlightNo + " (" + this.Date + "): " + this.Departure + "->" + this.Destination + " Utilization: " + this.FlightUtilization + "% booked: " + this.BookedUp;
}
public string PilotInfo { get; set; }
/// <summary>
/// Pilot 1:1
/// </summary>
public PilotView Pilot { get; set; }
/// <summary>
/// Passengers 1:n
/// </summary>
public List<PassengerView> Passengers{ get; set; }
}
public class PilotView
{
public int PersonId { get; set; }
public string Surname { get; set; }
public DateTime Birthday { get; set; }
}
public class PassengerView
{
public PassengerView()
{
this.FlightViewSet = new HashSet<FlightView>();
}
public int PersonID { get; set; }
public Nullable<System.DateTime> CustomerSince { get; set; }
public int Birthday { get; set; }
public string GivenName { get; set; }
public string Surname { get; set; }
public virtual ICollection<FlightView> FlightViewSet { get; set; }
}
}
Listing 20-3ViewModel Classes
配置映射
在使用 AutoMapper 进行映射之前,您必须为每个应用域向 AutoMapper 中涉及的类注册一次映射。你用方法Initialize()
来做这个。
Attention
如果在一个方法中多次调用Initialize()
,只有最后一次执行的配置才有效!
在Initialize()
中,CreateMap()
方法用于两个类之间的具体映射定义。CreateMap()
需要两个类型参数。
- 第一个参数总是源类型。
- 二是目标类型。
如果您需要双向转换,您必须显式地创建它。
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Flight, FlightView>();
cfg.CreateMap<FlightView, Flight>();
cfg.CreateMap<Passenger, PassengerView>();
cfg.CreateMap<PassengerView, Passenger>();
cfg.CreateMap<Pilot, PilotDetailView>();
cfg.CreateMap<PilotDetailView, Pilot>();
});
或者,您可以使用ReverseMap()
方法在一行中创建两个方向的映射。
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Flight, FlightView>().ReverseMap();
cfg.CreateMap<Passenger, PassengerView>().ReverseMap();
cfg.CreateMap<Pilot, PilotDetailView>().ReverseMap();
});
在Initialize()
内多次调用CreateMap()
的顺序不相关。
对于单个类,可以有到一个类以及几个其他类的映射。这里有一个例子:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Flight, FlightView>();
cfg.CreateMap<Flight, FlightDTOShort>();
cfg.CreateMap<Flight, FlightDTO>();
});
然后实际使用这些映射中的哪一个(也就是说,在哪个目标类型上)由执行实际映射的Map()
方法的参数决定。
使用 Map()运行映射
AutoMapper 的Map()
方法有三个选项。
对于选项 1,您可以映射到一个新的对象,然后将目标类型指定为泛型类型参数,将源对象指定为方法的参数。
FlightView flightView1 = Mapper.Map<FlightView>(flight);
对于选项 2,如果您使用Map()
的非通用变体,程序代码会变得更加广泛。现在,在源对象后面输入源对象类型作为第二个参数,输入目标类型作为第三个参数。此外,Map()
只返回类型System.Object
,因此需要使用FlightView
进行类型转换。
FlightView FlightView2 = (FlightView) AutoMapper.Mapper .Map (Flight, Flight.GetType(), typeof ( FlightView ));
对于选项 3,将一个对象映射到另一个现有对象。这是Map()
的第三个变体。在这种情况下,该方法不需要类型参数,但是源和目标对象将作为参数进行传输。
var flightView3 = new FlightView();
flightView3.Memo = "test";
Mapper.Map(flight, flightView3);
使用非静态 API
除了静态 API,AutoMapper 还有一个非静态 API(实例 API)用于配置映射(清单 20-4 )。您配置了一个MapperConfiguration
类的实例,并使用它通过CreateMapper()
创建一个带有IMapper
接口的对象。然后这个对象有了Map()
方法。
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Flight, FlightView>();
cfg.CreateMap<Pilot, PilotView>();
cfg.CreateMap<Passenger, PassengerView>();
cfg.AddProfile<AutoMapperProfile2>();
});
config.AssertConfigurationIsValid();
IMapper mapper = config.CreateMapper();
var flightView4 = mapper.Map<Flight, FlightView>(flight);
Listing 20-4Nonstatic API
映射约定
AutoMapper 不仅映射同名的属性,还包含其他标准约定,如下所示:
- 如果在源对象中没有找到属性
x
,则搜索一个GetX()
函数,如果有必要,就调用这个函数。 - 如果属性的名称包含多个大写字母,并且有依赖对象,那么每个单词都被理解为一个级别,并用句点分隔。例如,名称
obj.AircraftTypeDetailLength
是映射到obj.AircraftType.Detail.Length
的。AutoMapper 称这个特性为扁平化。 - AutoMapper 忽略任何空引用运行时错误。
- AutoMapper 访问私有的 getter 和 setter,但前提是单个 getter 或 setter 是私有的。如果整个属性被声明为私有,它将被忽略。
- 在 AutoMapper 中,对大写和小写字母以及下划线的处理非常令人兴奋。AutoMapper 也在变化的大小写中寻找合适的属性,不管有没有下划线。甚至类中属性声明的顺序也是相关的!
表 20-1 显示了一个属性Free Spaces
有四种不同拼法的几种情况:FreeSeats
、freeSeats
、Free_Seats
和free_Seats
。总是假设源对象中只有FreePoints
属性的变体,它也在表的第 1 列中设置。此外,目标对象始终包含属性的所有四种变体,并且属性的顺序与它们在表中的顺序相同。
表 20-1
AutoMappers Convention-Based Mapping Behavior Regarding Underscore and Case
| 源对象中的值 | 目标对象中的值 | 评论 | | :-- | :-- | :-- | | `f.FreeSeats = 1` | `f.FreeSeats = 1``f.freeSeats = 1``f.Free_Seats = 1` | AutoMapper 将源对象中一个属性的值复制到目标对象的所有四个变量中。 | | `f.FreeSeats = 1` `f.freeSeats = 2` | `f.FreeSeats = 1``f.freeSeats = 1``f.Free_Seats = 1` | 属性`FreeSeats`的值被忽略,因为`FreeSeats`已经将其值映射到所有目标属性。 | | `f.FreeSeats = 1``f.freeSeats = 2` | `f.FreeSeats = 1``f.freeSeats = 1``f.Free_Seats = 3` | 第一个不带下划线的属性映射到所有不带下划线的属性,第一个带下划线的属性映射到所有带下划线的属性。 | | `f.FreeSeats = 1``f.freeSeats = 2` | `f.FreeSeats = 1``f.freeSeats = 1``f.Free_Seats = 4` | 第一个不带下划线的属性映射到不带下划线的属性,第一个带下划线的属性映射到所有带下划线的属性。 | | `f.FreeSeats = 1``f.freeSeats = 2``f.Free_Seats = 3` | `f.FreeSeats = 1``f.freeSeats = 1``f.Free_Seats = 3` | 第一个不带下划线的属性映射到所有不带下划线的属性,第一个带下划线的属性映射到所有带下划线的属性。 |清单 20-5 展示了如何将一个Flight
对象映射到FlightView
。图 20-7 显示输出。
图 20-7
Output of Listing 20-5
public class AutoMapperBasics
{
public static void Demo_SingleObject()
{
CUI.Headline(nameof(Demo_SingleObject));
// take the first flight as an example
var ctx = new WWWingsContext();
var flight = ctx.FlightSet.Include(x=>x.Pilot).Include(x => x.AircraftType).ThenInclude(y=>y.Detail).FirstOrDefault();
Console.WriteLine(flight);
//##################################################
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Flight, FlightView>();
cfg.CreateMap<FlightView, Flight>();
cfg.CreateMap<Passenger, PassengerView>();
cfg.CreateMap<PassengerView, Passenger>();
cfg.CreateMap<Pilot, PilotDetailView>();
cfg.CreateMap<PilotDetailView, Pilot>();
cfg.CreateMap<Flight, FlightDTOShort>();
cfg.CreateMap<Flight, FlightDTO>();
});
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Flight, FlightView>().ReverseMap();
cfg.CreateMap<Passenger, PassengerView>().ReverseMap();
cfg.CreateMap<Pilot, PilotDetailView>().ReverseMap();
});
Mapper.Initialize(cfg =>
{
cfg.SourceMemberNamingConvention = new NoNamingConvention();
cfg.DestinationMemberNamingConvention = new NoNamingConvention();
cfg.CreateMap<Flight, FlightView>().ReverseMap();
cfg.CreateMap<Passenger, PassengerView>().ReverseMap();
cfg.CreateMap<Pilot, PilotDetailView>().ReverseMap();
cfg.CreateMap<Flight, FlightDTOShort>();
cfg.CreateMap<Flight, FlightDTO>();
});
Mapper.Initialize(cfg =>
{
cfg.AddProfile<AutoMapperProfileEinfach>();
});
Mapper.Initialize(cfg =>
{
cfg.AddProfile<AutoMapperProfileKomplex>();
});
// ----------------------
CUI.Headline("Mapping to new object");
FlightView flightView1 = Mapper.Map<FlightView>(flight);
Console.WriteLine(flightView1);
Console.WriteLine(flightView1.PilotSurname);
Console.WriteLine(flightView1.SmokerInfo);
Console.WriteLine(flightView1.PilotInfo);
if (flightView1.Pilot == null) CUI.PrintError("No pilot!");
else
{
Console.WriteLine(flightView1.Pilot?.Surname + " born " + flightView1.Pilot?.Birthday);
}
Console.WriteLine(flightView1.Memo);
Console.WriteLine(flightView1.AircraftTypeDetailLength);
FlightView flightView2 = (FlightView)Mapper.Map(flight, flight.GetType(), typeof(FlightView));
Console.WriteLine(flightView2);
Console.WriteLine(flightView2.PilotSurname);
Console.WriteLine(flightView2.SmokerInfo);
Console.WriteLine(flightView2.PilotInfo);
if (flightView2.Pilot == null) CUI.PrintError("No pilot!");
else
{
Console.WriteLine(flightView2.Pilot?.Surname + " born " + flightView2.Pilot?.Birthday);
}
Console.WriteLine(flightView2.AircraftTypeDetailLength);
Console.WriteLine(flightView2.Memo);
// ----------------------
CUI.Headline("Mapping to existing object");
var flightView3 = new FlightView();
Mapper.Map(flight, flightView3);
Console.WriteLine(flightView3);
Console.WriteLine(flightView3.PilotSurname);
Console.WriteLine(flightView3.SmokerInfo);
Console.WriteLine(flightView3.PilotInfo);
if (flightView3.Pilot == null) CUI.PrintError("No pilot!");
else
{
Console.WriteLine(flightView3.Pilot?.Surname + " born " + flightView3.Pilot?.Birthday);
}
Console.WriteLine(flightView3.Memo);
}
}
Listing 20-5Mapping from Flight to FlightView
更改映射约定
您可以覆盖接受下划线作为分隔符的约定。为此,你可以编写自己的约定类(参见清单 20-6 ,它通过将SeparatorCharacter
设置为空字符串并且在SplittingExpression
中不使用正则表达式来覆盖下划线的呈现。
using AutoMapper;
using System.Text.RegularExpressions;
namespace EFC_Console.AutoMapper
{
/// <summary>
/// No use of underscores when mapping
/// </summary>
class NoNamingConvention : INamingConvention
{
#region INamingConvention Members
public string ReplaceValue(Match match)
{
return "";
}
public string SeparatorCharacter
{
get { return ""; }
}
public Regex SplittingExpression
{
get { return new Regex(""); }
}
#endregion
}
}
Listing 20-6A Separate Convention Class for AutoMapper That Overrides the Rendering of Underscores
您自己的约定必须包含在配置中,如下所示:
Mapper.Initialize(cfg =>
{
cfg.SourceMemberNamingConvention = new NoNamingConvention();
cfg.DestinationMemberNamingConvention = new NoNamingConvention();
cfg.CreateMap<Flight, FlightView>().ReverseMap();
cfg.CreateMap<Passenger, PassengerView>().ReverseMap();
cfg.CreateMap<Pilot, PilotDetailView>().ReverseMap();
});
配置文件类别
您可以将 AutoMapper 配置外包给所谓的 profile 类,这些类继承自基类Profile
。
using AutoMapper;
using BO;
namespace EFC_Console.AutoMapper
{
/// <summary>
/// Simple profile class for AutoMapper
/// </summary
public class AutoMapperProfile1 : Profile
{
public AutoMapperProfile1()
{
this.SourceMemberNamingConvention = new NoNamingConvention();
this.DestinationMemberNamingConvention = new NoNamingConvention();
this.CreateMap<Flight, FlightView>().ReverseMap();
this.CreateMap<Passenger, PassengerView>().ReverseMap();
this.CreateMap<Pilot, PilotDetailView>().ReverseMap();
this.CreateMap<Flight, FlightDTOShort>();
this.CreateMap<Flight, FlightDTO>();
}
}
}
然后通过AddProfile()
在Initialize()
中调用这个配置文件类。
Mapper.Initialize(cfg =>
{
cfg.AddProfile<AutoMapperProfile1>();
});
忽略子对象
如果源对象和目标对象都有一个子对象,并且属性名称根据某个自动映射器约定进行映射,但是没有该对象类型的映射,则自动映射器会报错以下错误:“缺少类型映射配置或不支持的映射。”要么您必须为子对象类型创建一个带有CreateMap()
的映射,要么您必须明确地告诉 AutoMapper 您不想映射子对象。
在调用ForMember()
之后,通过使用Ignore()
方法的CreateMap()
方法的流畅 API 来完成忽略。
AutoMapper.Mapper.CreateMap<Passenger, PassengerView>().ForMember(z => z.PilotView, m => m.Ignore());
Note
对子对象调用Ignore()
后,展平仍然有效。也就是说,AutoMapper
将继续用来自Pilot.Surname
的值填充类FlightView
中的属性PilotSurname
,即使flightView.PilotView
被Ignore()
语句设置为空。
自定义映射
AutoMapper 的开发者提供了许多操作地图的方法。使用CreateMap()
方法的 Fluent API,您可以定义源对象属性到目标对象属性的映射,称为投影。手动映射使用ForMember()
方法。要指定的第一个参数是目标属性的 lambda 表达式(目标的变量名z
),第二个参数是值的 lambda 表达式(源的变量名q
),由此可以引用源对象的一个或多个属性。
清单 20-7 显示了以下十种可能性:
- 使用
UseValue()
将属性映射到静态值。 - 使用
MapFrom(
将一个属性映射到一个源属性的表达式的结果,其中结果是一个布尔值。 - 使用
MapFrom()
将一个属性映射到多个源属性的计算,其中值是一个数字。 - 将带有
MapFrom()
的属性映射到源对象的子对象中的ToString()
方法的结果。 - 将带有
MapFrom()
的属性映射到包含来自多个源属性的值的对象。 - 使用
ResolveUsing()
和IValueResolver
接口将属性映射到一个ValueResolver
类。 - 使用
NullSubstitute()
方法将零值映射到另一个值。 - 指定目标对象的属性不得被源对象的值覆盖。这是通过
UseDestinationValue()
方法完成的。 - 倒数第二个例子展示了只有当源值(
SourceValue
)满足特定条件时Condition()
如何映射。 - 最后一种情况显示了从 N:M 映射到 1:N 映射的转换。这里删除了连接
Flight
和Passenger
的中间实体booking
。目标类FlightView
有一个List<Passenger>
类型的属性。
因为这样的映射定义经常会变得非常广泛,所以通常建议将它们外包给一个 profile 类(参见清单 20-7 )而不是分散在程序代码中的某个地方。清单 20-8 显示了解析器类。
public AutoMapperProfile2()
{
#region Mappings for class Flight
CreateMap<Flight, FlightView>()
// 1\. Set Memo to static value
.ForMember(z => z.Memo,
q => q.UseValue("Loaded from Database: " + DateTime.Now))
// 2\. Mapping for a bool property
.ForMember(z => z.BookedUp, q => q.MapFrom(f => f.FreeSeats <= 0))
// 3\. Mapping with calculation
.ForMember(z => z.FlightUtilization,
q => q.MapFrom(f => (int)Math.Abs(((decimal)f.FreeSeats / (decimal)f.Seats) * 100)))
// 4\. Mapping to a method result
.ForMember(z => z.PilotInfo, m => m.MapFrom(
q => q.Pilot.ToString()))
// 5\. Mapping to a method result with object construction
.ForMember(z => z.Pilot,
m => m.MapFrom(
q => new Pilot { PersonID = q.Pilot.PersonID, Surname = q.Pilot.FullName, Birthday = q.Pilot.Birthday.GetValueOrDefault() }))
// 6\. Mapping with a value resolver
.ForMember(z => z.SmokerInfo,
m => m.ResolveUsing<SmokerInfoResolver>())
// 7\. Mapping if source value is null
.ForMember(z => z.Destination, q => q.NullSubstitute("unknown"))
// 8\. No Mapping for existing values
.ForMember(z => z.Timestamp, q => q.UseDestinationValue())
// 9\. Conditional Mapping
.ForMember(z => z.Seats, x => x.Condition(q => q.FreeSeats < 250))
// 10\. Map n:m to zu 1:n (for Flight->Booking->Passenger)
.ForMember(dto => dto.PassengerViewSet, opt => opt.MapFrom(x => x.BookingSet.Select(y => y.Passenger).ToList()))
// 11\. Include reverse Mapping
.ReverseMap();
#endregion
#region Other class mappings
CreateMap<Pilot, string>().ConvertUsing<PilotStringConverter>();
// Map n:m to zu 1:n (for Passenger->Booking->Flight)
CreateMap<Passenger, PassengerView>()
.ForMember(z => z.FlightViewSet, m => m.MapFrom(q => q.BookingSet.Select(y => y.Flight)));
#endregion
#region Typkonvertierungen
CreateMap<byte, long>().ConvertUsing(Convert.ToInt64);
CreateMap<byte, long>().ConvertUsing(ConvertByteToLong);
#endregion
}
Listing 20-7Manual AutoMapper Mappings with ForMember( )
namespace EFC_Console.AutoMapper
{
/// <summary>
/// Value Resolver for Automapper, converts true/false to
/// string property "SmokerInfo"
/// </summary>
public class SmokerInfoResolver : IValueResolver<Flight, FlightView, string>
{
public string Resolve(Flight source, FlightView destination, string member, ResolutionContext context)
{
if (source.NonSmokingFlight.GetValueOrDefault()) destination.SmokerInfo = "This is a non-smoking flight!";
else destination.SmokerInfo = "Smoking is allowed.";
return destination.SmokerInfo;
}
}
}
Listing 20-8A Value Resolver Class for AutoMapper
类型转换
当映射基本数据类型(string
、int
、decimal
、bool
等)时,如果类型相同或者目标类型为string
,AutoMapper 很容易映射。在目标类型为string
的情况下,AutoMapper 总是可以通过调用ToString()
获得一个字符串。Number
类型自动上下转换。这允许自动映射器从byte
映射到long
,也可以从long
映射到byte
。但是,从 4.0 版开始,这种灵活性就有了。版本 3.3.1 对将long
映射到byte
的尝试做出响应,出现以下错误:“缺少类型映射配置或不支持的映射。映射类型:系统。Byte - > System.Int64 . "同样在 AutoMapper 4.0 中,如果要映射的值不适合目标数字类型,则会出现以下运行时错误:" AutoMapper。AutoMapperMappingException:值对于无符号字节太大或太小。
当然,如果类型完全不同,AutoMapper 就不能映射。例如,如果属性Birthday
在源对象中具有类型DateTime
,但是在目标对象中使用了Integer
,那么运行时错误将总是发生(AutoMapper.AutoMapperMappingException
)。在错误消息中,您将找到有关该问题的详细信息,如下所示:
- 系统。日期时间➤系统。Int32
- 目标路径:
- PassengerView。生日
- 源值:
- 01.10.1980 00:00:00
对于 AutoMapper 不能自动执行的类型图像,或者与 AutoMapper 在标准版本中不同的类型图像,您必须为 AutoMapper 提供一个类型转换器(清单 20-9 和清单 20-10 )。这样的类型转换器可以用一个简单的方法实现,该方法接受类型x
并返回y
。然后,这个转换器方法被注册到 AutoMapper。
CreateMap<byte, long>().ConvertUsing(ConvertByteToLong);
CreateMap<DateTime, Int32>().ConvertUsing(ConvertDateTimeToInt);
如有必要,可以调用。NET 框架。
CreateMap<byte, long>().ConvertUsing(Convert.ToInt64);
/// <summary>
/// Converts bytes to long with special case 0
/// </summary>
/// <param name="b">Byte value</param>
/// <returns></returns>
public static long ConvertByteToLong(byte b)
{
if (b == 0) return -1;
else return (long) b;
}
Listing 20-9Method-Based Type Converter for AutoMapper
/// <summary>
/// Converts bytes to long with special case 0
/// </summary>
/// <param name="d">DateTime value</param>
/// <returns></returns>
public static Int32 ConvertDateTimeToInt(DateTime d)
{
return d.Year;
}
Listing 20-10Another Method-Based Type Converter for AutoMapper
您还可以使用Convert()
方法将类型转换器实现为实现ITypeConverter
接口的类(参见清单 20-11 )。然后这个定制的转换器类被注册到一个通用的ConvertUsing()
变量中。
CreateMap<Pilot, string>().ConvertUsing<PilotStringConverter>();
/// <summary>
/// Converts a Pilot to a string
/// </summary>
public class PilotStringConverter : ITypeConverter<Pilot, string>
{
public string Convert(Pilot pilot, string s, ResolutionContext context)
{
if (pilot == null) return "(Not assigned)";
return "Pilot # " + pilot.PersonID;
}
}
}
Listing 20-11Class-Based Type Converter for AutoMapper
到目前为止,显示的转换对于所有类中的所有图像都是全局的。这当然是一个强大的特性,因为它避免了重复一些映射。但是您在这里也应该小心,因为您可能会创建不想要的图像,这样数据可能会丢失。
也可能是您根本不希望在全局范围内进行这样的转换,而只是希望在单个类中进行单个属性图像的转换。在这种情况下,你可以写一个ValueResolver
(参见‘自定义映射’前的子章节)。
收集
即使您总是配置 AutoMapper 来映射单个类,AutoMapper 不仅可以映射单个实例,还可以使用Map()
将任意数量的这些类相互映射。
这里有一个例子:
List<FlightView> FlightviewList = AutoMapper.Mapper.Map <List<Flight View >> (Flight list);
AutoMapper 支持以下类型的卷(列表 20-12 ):
IEnumerable
IEnumerable <T>
ICollection
ICollection <T>
IList
IList<T>
List<T>
- 数组
public static void Demo_ListMapping()
{
CUI.Headline(nameof(Demo_ListMapping));
Mapper.Initialize(cfg =>
{
cfg.AddProfile<AutoMapperProfile2>();
});
using (var ctx2 = new WWWingsContext())
{
var flightSet = ctx2.FlightSet.Include(f => f.Pilot).Include(f => f.BookingSet).ThenInclude(x => x.Passenger).Where(f => f.Departure == "Berlin").OrderBy(f => f.FlightNo).Take(5).ToList();
// map all objects in this list
List<FlightView> flightviewListe = Mapper.Map<List<FlightView>>(flightSet);
foreach (var f in flightviewListe)
{
Console.WriteLine(f.ToString());
if (f.Passengers!= null)
{
foreach (var pas in f.PassengerViewSet)
{
Console.WriteLine(" - " + pas.GivenName + " " + pas.Surname + " has " + pas.FlightViewSet.Count + " Flights!");
}
}
}
}
}
Listing 20-12Mapping of an Entire List
继承
为了说明自动映射器在继承关系中的行为,清单 20-13 中的例子使用了Person
、Woman
和Man
类,以及相关的数据传输对象(DTO)类PersonDTO
、MsDTO
和MannDTO
。根据一句老话,Man
和Woman
的区别是基于拥有大量的汽车(在Man
中)或鞋子(在Woman
中)。DTO 类通过数值的数据类型(字节而不是整数)以及名和姓的组合作为属性名来区分。此外,DTO 类中的生日只保存年份,而不是完整的日期。
class Person
{
public string GivenName { get; set; }
public string Surname { get; set; }
public DateTime Birthday { get; set; }
}
class Man : Person
{
public int NumberOfCars { get; set; }
}
class Woman : Person
{
public int NumberOfShoes { get; set; }
}
class PersonDTO
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
}
class ManDTO : PersonDTO
{
public byte NumberOfCars{ get; set; }
}
class WomanDTO : PersonDTO
{
public byte NumberOfShoes{ get; set; }
}
Listing 20-13Class Hierarchy for the Inheritance Example
基本上,您必须为继承关系中的继承层次结构中的每个单独的类定义一个映射。
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>();
cfg.CreateMap<Woman, WomanDTO>();
cfg.CreateMap<Man, ManDTO>();
});
虽然 AutoMapper 自动处理以字节为单位的类型转换整数,但缺少姓名和出生日期的映射。出生日期的类型冲突会导致映射期间的运行时错误(AutoMapper.AutoMapperMappingException
)。
仅在基类上用ForMember()
和MapFrom()
设置手动映射是不够的。
cfg.CreateMap<Person, PersonDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Woman, WomanDTO>();
cfg.CreateMap<Man, ManDTO>();
之后,只有类Person
的映射是正确的。类Man
和Woman
继续产生运行时错误。AutoMapper 希望继承层次结构中的每个类都有手动映射配置,如下所示:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Man, ManDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Woman, WomanDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
});
但是您可以通过使用 AutoMapper 的Include()
方法来避免这种程序代码重复(不要与实体框架的Include()
方法混淆!).
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.Include<Man, ManDTO>()
.Include<Woman, WomanDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Man, ManDTO>();
cfg.CreateMap<Woman, WomanDTO>();
});
清单 20-14 展示了一个Person
、Man
和Woman
映射的例子,包括一个男人到一个女人的性别转换,汽车的数量被转换成鞋子数量的十倍。在定义了这个映射之后,使用 AutoMapper 从Man
到Woman
的实际转换相对来说是比较容易的。
public static void Inheritance()
{
CUI.Headline(nameof(Inheritance));
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Man, ManDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Woman, WomanDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
});
// or shorter using include()
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.Include<Man, ManDTO>()
.Include<Woman, WomanDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
cfg.CreateMap<Man, ManDTO>();
cfg.CreateMap<Woman, WomanDTO>();
});
var m = new Man()
{
GivenName = "John",
Surname = "Doe",
Birthday = new DateTime(1980, 10, 1),
NumberOfCars = 40
};
PersonDTO mDTO1 = Mapper.Map<PersonDTO>(m);
Console.WriteLine(mDTO1.Name + " *" + mDTO1.YearOfBirth);
ManDTO mDTO1b = Mapper.Map<ManDTO>(m);
Console.WriteLine(mDTO1b.Name + " *" + mDTO1b.YearOfBirth);
ManDTO mDTO2 = (ManDTO)Mapper.Map(m, m.GetType(), typeof(ManDTO));
Console.WriteLine(mDTO2.Name + " *" + mDTO2.YearOfBirth + " owns " + mDTO2.NumberOfCars + " cars.");
ManDTO mDTO3 = Mapper.Map<ManDTO>(m);
Console.WriteLine(mDTO3.Name + " *" + mDTO3.YearOfBirth + " owns " + mDTO3.NumberOfCars + " cars.");
// gender transformation: man -> woman
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Man, Woman>()
.ForMember(z => z.NumberOfShoes, map => map.MapFrom(q => q.NumberOfCars * 10));
});
Woman f = Mapper.Map<Woman>(m);
Console.WriteLine(f.GivenName + " " + f.Surname + " *" + f.Birthday + " owns " + f.NumberOfShoes + " shoes.");
}
Listing 20-14Mapping with Person, Man, and Woman
如果一个派生类有一个与基类映射相矛盾的手动映射会发生什么?根据文档( https://github.com/AutoMapper/AutoMapper/wiki/Mapping-inheritance
),评估由 AutoMapper 按照以下优先级进行:
- 派生类中的显式映射
- 继承的显式映射
- 与
Ignore()
的映射 - 仅在最后一步起作用的自动映射约定
通用类
AutoMapper 也有助于泛型类。对于 AutoMapper 来说,映射泛型列表是非常基本的(参见清单 20-15 )。
public static void GenericHomogeneousList()
{
CUI.Headline(nameof(GenericHomogeneousList));
var PersonSet = new List<Person>();
for (int i = 0; i < 100; i++)
{
PersonSet.Add(new Person() { GivenName="John", Surname="Doe"});
}
// define Mapping
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
});
// Convert list
var PersonDTOSet = Mapper.Map<List<PersonDTO>>(PersonSet);
Console.WriteLine(PersonDTOSet.Count());
foreach (var p in PersonDTOSet.Take(5))
{
Console.WriteLine(p.Name + ": "+ p.YearOfBirth);
}
}
Listing 20-15Mapping Generic Lists, Here the Example List<T>
AutoMapper 还可以将整个泛型类型映射到其他泛型类型。坚持以Person
、Woman
和Man
为例,清单 20-16 定义了两种常见的合伙类型:注册合伙和婚姻。
// see https://europa.eu/youreurope/citizens/family/couple/registered-partners/index_en.htm
class RegisteredPartnership<T1, T2>
where T1 : Person
where T2 : Person
{
public T1 Partner1 { get; set; }
public T2 Partner2 { get; set; }
public DateTime Date { get; set; }
}
class Marriage<T1, T2>
where T1 : Person
where T2 : Person
{
public T1 Partner1 { get; set; }
public T2 Partner2 { get; set; }
public DateTime Date { get; set; }
}
Listing 20-16Generic Types for Partnership and Marriage
这允许同性登记的伙伴关系,以及同性婚姻!你甚至可以允许登记的伴侣关系自动转变为婚姻。例如,将registered partnership <husband, husband>
的类型转换为marriage <husband, husband>
对于 AutoMapper 来说没有问题。您所要做的就是在泛型类之间定义一个通用映射。您决不需要为这些泛型类的类型参数的所有可能变体编写一个映射。
Mapper.Initialize (cfg =>
{
cfg.CreateMap (typeof (RegisteredPartnership <,>), typeof (Marriage <,>));
30.4
清单 20-17 展示了这种映射的应用。
// A registered partnership between two men
var m1 = new Man() {first name = "Heinz" , last name = "Müller" };
var m2 = new Man() {first name = "Gerd" , last name = "Meier" };
var ep = new RegisteredPartnership < Man , Man >() {Partner1 = m1, Partner2 = m2, Date = new DateTime (2015,5,28)};
// The general mapping between the generic classes
Mapper.Initialize (cfg =>
{
cfg.CreateMap (typeof (RegisteredPartnership <,>), typeof (Marriage <,>));
30.4
// Then every figure with concrete type parameters is allowed!
Marriage < husband , husband > marriage = AutoMapper.Mapper .Map < marriage < man , man >> (ep);
Console .WriteLine (before.Partner1.Name + "+" + marriage.Partner2.Name + ":" + marriage.- DateToShortDateString());
Listing 20-17Mapping Your Own Generic Types from Listing 20-16
通用参数的附加映射是可能的,例如RegisteredPartnership<man, man>
到Marriage <ManDTO, ManDTO>
。当然,这需要做到以下几点:
-
泛型类 marriage
<T1, T2>
作为类型参数也允许使用ManDTO
或PersonDTO
(它还没有这么做)。所以,你必须像这样改变它: -
还需要定义
Man
和ManDTO
之间的映射,如下所示:
class Marriage<T1, T2>
where T1 : PersonDTO
where T2 : PersonDTO
{
public T1 Partner1 { get; set; }
public T2 Partner2 { get; set; }
public DateTime Date { get; set; }
}
Mapper.Initialize(cfg =>
{
cfg.CreateMap(typeof(RegisteredPartnership<,>), typeof(Marriage<,>));
cfg.CreateMap<Man, ManDTO>()
.ForMember(z => z.NumberOfCars, map => map.MapFrom(q => q.GivenName + " " + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year));
});
之后就可以做RegisteredPartnership<Man, Man>
和Marriage<ManDTO, ManDTO>
的映射了。
Marriage<ManDTO, ManDTO> marriageDTO = Mapper.Map<Marriage<ManDTO, ManDTO>>(ep);
Console.WriteLine(marriageDTO.Partner1.Name + " + " + marriageDTO.Partner2.Name + ": " + marriage.Date.ToShortDateString());
映射前后的附加操作
AutoMapper 允许在映射之前或之后执行映射操作。CreateMap()
的 Fluent API 中的BeforeMap()
方法设置上游动作;AfterMap()
集下游。两种方法都可以被多次调用,如清单 20-18 以BeforeMap()
为例所示。这两种方法都需要一个表达式来获取源对象(在清单中简称为q
)和目标对象(在清单中简称为z
)。可以调用一个方法作为表达式的一部分。这也在清单 20-18 中以AfterMap()
为例进行了展示。
清单 20-18 中的示例在将Person
映射到PersonDTO
时执行以下操作:
- 如果名字或姓氏为空,源对象中的条目将替换为问号。
- 如果名字
??
因为名字和姓氏为空而出现在目标对象中,则值"error"
或"no information"
将根据出生年份来传递。背后的业务流程规则如下:所有 1980 年之前出生的人都可以匿名。此后出生的所有人都要起一个名字。如果仍然缺少名称,一定是出错了。
当然,您可以在 AutoMapper 之外设置这样的业务逻辑。这些是与 AutoMapper 集成的优势:
- 您可以在一个地方完成所有映射操作。
- 您不必在映射之前或之后对对象的迭代进行显式预编程。
public static void BeforeAfterDemo()
{
CUI.Headline(nameof(BeforeAfterDemo));
var PersonSet = new List<Person>();
for (int I = 0; i < 10; i++)
{
PersonSet.Add(new Person()
{
GivenName =""Joh"",
Surname =""Do"",
Birthday = new DateTime(1980, 10, 1),
});
}
// Define mapping
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Person, PersonDTO>()
.ForMember(z => z.Name, map => map.MapFrom(q => q.GivenName +"""" + q.Surname))
.ForMember(z => z.YearOfBirth, map => map.MapFrom(q => q.Birthday.Year))
.BeforeMap((q, z) => q.GivenName = (String.IsNullOrEmpty(q.GivenName) ? q.GivenName ="""" : q.GivenName))
.BeforeMap((q, z) => q.Surname = (String.IsNullOrEmpty(q.Surname) ? q.Surname ="""" : q.Surname))
.AfterMap((q, z) => z.Name = GetName(z.Name, z.YearOfBirth));
cfg.CreateMap<DateTime, Int32>().ConvertUsing(ConvertDateTimeToInt);
});
// Map list
var PersonDTOSet = Mapper.Map<List<PersonDTO>>(PersonSet);
foreach (var p in PersonDTOSet)
{
Console.WriteLine(p.Name +"" in born in year"" + p.YearOfBirth);
}
}
/// <summary>
/// Converges DateTime into integer (only extracts year)
/// </summary>
/// <returns></returns>
public static Int32 ConvertDateTimeToInt(DateTime d)
{
return d.Year;
}
/// <summary>
/// Method called as part of AfterMap()
/// </summary>
/// <param name”"”">Surname</param>
/// <param name""yearOfBirt"">YearOfBirth</param>
/// <returns></returns>
public static string GetName(string name, int yearOfBirth)
{
if (yearOfBirth == 0) return name;
if (yearOfBirth <= 1980) return name +"" (too young"";
return name +"" "" + yearOfBirth """";
}
Listing 20-18BeforeMap( )and AfterMap( ) in Action
表演
AutoMapper 不为每次提取和映射值集使用反射。相反,CreateMap()
使用Reflection.Emit()
在运行时生成程序代码。这就提出了绘制大量数据地图需要多长时间的问题。
在表 20-2 中,比较了三种映射路径。
- 显式、硬编码的对象-对象映射(即
xa = ya
,用于每个属性) - 基于反射的对象-对象映射,包含本章开头的程序代码
- 使用自动映射器的对象-对象映射
为了避免将苹果与梨进行比较,在表 20-2 中,两个完全相同的构造类型之间存在映射。换句话说,所有属性在两个类中都以相同的方式调用,并且具有相同的数据类型。性能测试测量通用列表中 1、10、100、1,000、10,000 和 100,000 个对象的值。
当查看结果表时,很明显 AutoMapper 要慢得多。用CreateMap()
生成映射代码总是需要大约 208 毫秒。如果一个类型的映射在一个流程中重复出现,这种情况只会出现一次。重复呼叫大约需要 7 毫秒。然而,AutoMapper 在所有情况下都比显式映射慢,对于多达 1,000 个对象的数据集,甚至比基于反射的映射更慢。
表 20-2
Speed Comparison of Three Methods for Object-to-Object Mapping (in Milliseconds)
| 对象数量 | 显式(硬编码)映射 | 反射映射 | 自动驾驶 | | :-- | :-- | :-- | :-- | | | 每个应用域一次性初始化工作 | 制图工作 | 每个应用域一次性初始化工作 | 制图工作 | 每个应用域一次性初始化工作 | 制图工作 | | one | Zero | Zero | Zero | Zero | Two hundred and eight | Eighteen | | Ten | Zero | Zero | Zero | Zero | Two hundred and eight | Eighteen | | One hundred | Zero | Zero | Zero | one | Two hundred and eight | Eighteen | | One thousand | Zero | Zero | Zero | Ten | Two hundred and eight | Nineteen | | Ten thousand | Zero | one | Zero | One hundred and four | Two hundred and eight | Thirty | | One hundred thousand | Zero | Twenty-nine | Zero | One thousand and ten | Two hundred and eight | Sixty-three |自动映射器的结论
AutoMapper 在不同的对象结构之间提供了灵活的成像选项。AutoMapper 的表现乍一看非常令人失望。但是,您一定不要忘记,与显式映射相比,AutoMapper 节省了大量的编程工作,并且可以做比反射映射更多的事情。
然而,像 Andrew Harcourt ( http://www.uglybugger.org/software/post/friends_dont_let_friends_use_automapper
)这样的开发者不仅批评 AutoMapper 的性能,而且不喜欢这些约定。当您重命名一个将要被映射的属性时,映射同名的属性就成了一个问题,除非您也考虑用ForMember()
编写一个定制的映射。Harcourt 提倡对所有映射进行显式编程,这使得自动名称重构成为可能。为了减少显式映射的编程工作,他为匹配的映射代码编写了一个代码生成器。不幸的是,他没有向公众提供代码生成器。
生成显式映射的工具包括 OTIS-LIB ( http://code.google.com/p/otis-lib
)和 Wayne Hartmann 的对象到对象映射实用程序( http://waynehartman.com/download?file=d2333998-c0cc-4bd4-8f02-82bef57d463c
)。然而,并不是每个人都喜欢生成器生成的程序代码。因此,这一章结束时没有明确的支持或反对 AutoMapper 的建议。这取决于应用(对象的大小和数量)和您自己的偏好。
二十一、案例研究
这一章描述了实体框架核心的一些实际应用,这些应用不在 World Wide Wings 示例中。
在 ASP.NET 核心应用中使用实体框架核心
2015 年,微软斥资逾 1 亿美元收购了总部位于柏林的应用发行商 Wunderlist ( https://www.theverge.com/2015/6/2/8707883/microsoft-wunderlist-acquisition-announced
)。MiracleList 是 Wunderlist 任务管理应用的重新编程,作为一个 web 应用和跨平台应用,用于 Windows、Linux、macOS、Android 和 iOS,并在云中提供跨平台后端。参见图 A-1 ,图 A-2 ,图 A-3 。
登录用户可以创建任务类别列表,然后在每个类别中创建任务列表。任务由标题、注释、输入日期和截止日期组成,并且可以标记为完成。除了 Wunderlist 的功能之外,在 MiracleList 中,一个任务可以有三个重要级别(A、B 或 C ),而不是只有两个重要级别(是/否)和一个努力级别(数字)。这种努力没有度量单位;用户可以决定是否将工作量设置为小时、天或相对值,如 1(代表低)到 10(代表高)。
与 Wunderlist 一样,任务可以有子任务,子任务只有一个标题和一个状态。MiracleList 中缺少原始版本的一些细节,例如上传文件到任务,在类别之间移动任务,搜索标签,复制和打印列表,以及在用户之间交换任务的能力。有些功能,如任务文本中的可点击超链接,没有被实现来防止误用。
图 A-3
MiracleList client for Android
图 A-2
MiracleList desktop client for Windows
图 A-1
MiracleList web application
除了 web API 之外,后端还有一个 web 接口,它提供了以下功能(图 A-4 ):
图 A-4
Web interface of the back end
- web API 的版本信息
- web API 的 OpenAPI 规范
- web API 的帮助页面
- 请求客户端 ID 以创建您自己的客户端的能力
- 下载桌面客户端的能力
在本书中,只讨论了后端的摘录,因为在那里使用了实体框架核心。
您可以在以下网站找到更多信息:
- Web 应用及客户端下载:
http://www.miraclelist.net
- 后端:
https://miraclelistbackend.azurewebsites.net
- 后端源代码:
https://github.com/HSchwichtenberg/MiracleListBackend
- 前端源代码:
https://github.com/HSchwichtenberg/MiracleListClient
架构
MiracleList 使用以下技术:
- 后端:。NET 核心,C#,ASP.NET 核心 Web API,实体框架核心,SQL Azure,Azure Web App,Swagger/Swashbuckle。AspNetCore,应用洞察
- 前端:带 HTML 的 SPA,CSS,TypeScript,Angular,Bootstrap,MomentJS,ng2-datetime,angular2-moment,angular2-contextmenu,angular2-modal,electronic,Cordova
奇迹列表的后端在 https://miraclelistbackend.azurewebsites.net
可供任何人使用。它运行在 C# 6.0 和。NET Core 2.0,使用 SQL Azure 作为数据库,实体框架 Core 2.0 作为 OR 映射器,ASP.NET Core 2.0 作为 web 服务器框架。它作为一个 web 应用托管在微软的 Azure 云中。
MiracleList 后端提供了清晰的分层。该解决方案(见图 A-5 )包括以下内容:
图 A-5
Projects in the MiracleList back-end solution
- 业务对象(BOs):这包含用于实体框架核心的实体类。这些类是以这样一种方式特意实现的,即它们也可以用作 Web API 中的输入和输出类型,这意味着不需要使用 AutoMapper 或其他工具进行额外的对象到对象映射。
- 数据访问层(DAL):这一层实现实体框架核心上下文(
Context.cs
)。 - 业务逻辑(BL):这里,使用实体框架核心上下文实现后端功能的“管理器”类被实现。
- MiracleList_WebAPI:这里实现了 WebAPI 的控制器。
- EFTools:包含用于正向工程的实体框架核心工具。
- UnitTests:这包含了使用 XUnit 的单元测试。
实体
图 A-6 显示了 MiracleList 对象模型,由五个类和一个枚举类型组成。清单 A-1 ,清单 A-2 ,清单 A-3 ,清单 A-4 ,清单 A-5 显示实体。
图 A-6
MiracleList object model
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BO
{
/// <summary>
/// Entity class representing a task
/// Used on the server up to the WebAPI
/// Corresponding proxy class in TypeScript is used on client
/// </summary>
public class Task
{
public int TaskID { get; set; } // PK per Konvention
[MaxLength(250)] // alias: StringLength
public string Title { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
public DateTime? Due { get; set; }
public Importance? Importance { get; set; }
public string Note { get; set; }
public bool Done { get; set; }
public decimal? Effort { get; set; }
public int Order { get; set; }
// -------------- Navigation Properties
public List<SubTask> SubTaskSet { get; set; } // 1:N
[Newtonsoft.Json.JsonIgnore] // Do not serialize
public Category Category { get; set; }
public int CategoryID { get; set; } // optional: FK Property
}
}
Listing A-1
Task.cs
namespace BO
{
public enum Importance
{
A, B, C
}
}
Listing A-2
Importance.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace BO
{
/// <summary>
/// Entity class representing a subtask
/// Used on the server up to the WebAPI
/// Corresponding proxy class in TypeScript is used on client
/// </summary>
public class SubTask
{
public int SubTaskID { get; set; } // PK
[MaxLength(250)]
public string Title { get; set; }
public bool Done { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
// -------------- Navigation Properties
public Task Task { get; set; }
public int TaskID { get; set; }
}
}
Listing A-3
SubTask.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
/// <summary>
/// Entity class representing a category of tasks
/// Used on the server up to the WebAPI
/// Corresponding proxy class in TypeScript is used on client
/// </summary>
public class Category
{
public int CategoryID { get; set; } // PK
[MaxLength(50)]
public string Name { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
// -------------- Navigation Properties
public List<Task> TaskSet { get; set; }
[Newtonsoft.Json.JsonIgnore] // Do not serialize
public User User { get; set; }
[Newtonsoft.Json.JsonIgnore] // Do not serialize
public int UserID { get; set; }
}
}
Listing A-4
Category.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BO
{
/// <summary>
/// Entity class representing a category of tasks
/// Used on the server up to the WebAPI
/// Not used in the client!
/// </summary>
public class Client
{
public Guid ClientID { get; set; }
[StringLength(50)]
public string Name { get; set; }
[StringLength(50)]
public string Company { get; set; }
[StringLength(50)]
public string EMail { get; set; }
public DateTime Created { get; set; } = DateTime.Now;
public DateTime? Deleted { get; set; }
public string Memo { get; set; }
[StringLength(10)]
public string Type { get; set; }
// -------------- Navigation Properties
public List<User> UserSet { get; set; }
}
}
Listing A-5
Client.cs
实体框架核心上下文类
从DbContext
派生的上下文类对于四个实体类(列表 A-6 )总是有一个类型为DbSet<T>
的属性。在OnConfiguring()
方法中,UseSqlServer()
设置实体框架核心数据库提供者,传入连接字符串。连接字符串是作为公共静态类成员实现的,因此可以在外部设置。
在OnModelCreating()
方法中,在列上设置了额外的索引,允许进行搜索。此外,为所有实体类全局指定数据库中的表名不应该像属性dbSet<T>
那样命名,而应该像类那样命名(换句话说,表Task
而不是表TaskSet
)。例外的只是有[Table]
注释的类,所以你有机会设置与规则的个别偏差。
using BO;
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace DAL
{
/// <summary>
/// Context class for Entity Framework Core
/// Forms the DAL that is used the BL manager classes
/// </summary>
public class Context : DbContext
{
// Register the entity classes in the context
public DbSet<Client> ClientSet { get; set; }
public DbSet<User> UserSet { get; set; }
public DbSet<Task> TaskSet { get; set; }
public DbSet<Category> CategorySet { get; set; }
public DbSet<Log> LogSet { get; set; }
// This connection string is just for testing. Is filled at runtime from configuration file
public static string ConnectionString { get; set; } = "Data Source=.;Initial Catalog = MiracleList_TEST; Integrated Security = True; Connect Timeout = 15; Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False;Application Name=EntityFramework";
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer(Context.ConnectionString);
}
protected override void OnModelCreating(ModelBuilder builder)
{
// In this case, EFCore can derive the database schema from the entity classes by convention and annotation.
// The following Fluent API configurations only change the default behavior!
#region Mass configuration via model class
foreach (IMutableEntityType entity in builder.Model.GetEntityTypes())
{
// all table names = class names (as with EF 6.x),
// except the classes that have [Table] annotation
var annotation = entity.ClrType.GetCustomAttribute<TableAttribute>();
if (annotation == null)
{
entity.Relational().TableName = entity.DisplayName();
}
}
#endregion
#region Custom Indices
builder.Entity<Category>().HasIndex(x => x.Name);
builder.Entity<Task>().HasIndex(x => x.Title);
builder.Entity<Task>().HasIndex(x => x.Done);
builder.Entity<Task>().HasIndex(x => x.Due);
builder.Entity<Task>().HasIndex(x => new { x.Title, x.Due });
#endregion
}
}
}
Listing A-6Context.cs
ASP.NET 核心应用中上下文类的生存期
在基于 ASP.NET 和 ASP.NET 核心的 web 应用和 web 服务/web API 中使用实体框架核心时,上下文类实例的生存期不得超过处理单个 HTTP 请求的生存期。
对于每个传入的 HTTP 请求,ASP.NET 核心使用不同的线程创建控制器类的新实例。如果您要在多个 HTTP 请求中使用 context 类的实例,您将在不同的线程中使用 context 实例,这不支持它。context 类不是线程安全的,因此它不支持多线程。带有上下文实例的多线程会在运行时导致以下错误:“System。InvalidOperationException:“在前一个操作完成之前,在此上下文上开始了第二个操作。任何实例成员都不能保证是线程安全的。"
Note
此外,经典实体框架中的上下文类ObjectContext
和DbContext
不是线程安全的。然而,在经典的实体框架中,当使用多线程上下文时,没有明确的错误消息。实体框架深处的某个地方出现了奇怪的崩溃。
此外,您会遇到实体框架核心缓存的问题,因为第二个请求会在上下文实例中找到前一个请求的数据。但是,第二个 HTTP 请求可能会影响另一个用户。在基于用户的变量中设置实体框架核心上下文并不是一个好的解决方案,因为这会大大降低 web 应用的可伸缩性。
因此,将上下文实例的生存期限制为处理单个 HTTP 请求的生存期是合适的。作为 HTTP 请求的一部分,您可以创建一个或多个 context 类的实例,然后在请求完成时销毁这些实例。
本实践解决方案中所示的架构很好地解决了这一问题,而无需直接使用 ASP.NET/ASP.NET 核心控制器中的上下文类。这种使用是通过提供业务逻辑的管理器类间接发生的。
管理器类的每个实例都创建一个新的上下文实例。管理器类实例的生命周期依赖于WebAPI
控制器实例的生命周期。因此,上下文实例不会超出处理 HTTP 请求的范围。
业务逻辑
清单 A-7 展示了一个类TaskManager
的例子。这个实现基于通用基类EntityManagerBase <contextType, entity type>
。反过来,EntityManagerBase
是以DataManagerBase <contextType>
为基础的。
这两个助手类提供了基本的功能,这些功能并不总是需要在每个管理器类中实现。这包括以下(清单 A-7 ,清单 A-8 ,清单 A-9 ):
- 创建
Manager
类时创建上下文实例 - 调用
Dispose()
时销毁上下文实例 Update()
:将对象添加到“已修改”状态的上下文中,并保存更改New()
:添加新对象,直接保存新对象Remove()
:删除对象,直接执行删除IsLoaded()
:检查对象是否存在于本地缓存中
using System;
using System.Collections.Generic;
using System.Linq;
using BO;
using DAL;
using ITVisions.EFC;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;
using ITVisions.EFCore;
namespace BL
{
/// <summary>
/// Business Logic manager for Tasks entities
/// </summary>
public class TaskManager : EntityManagerBase<Context, Task>
{
// To manage the subtasks
private SubTaskManager stm = new SubTaskManager();
// Current user
private int userID;
/// <summary>
/// Instantiation specifying the user ID to which all operations in this instance refer
/// </summary>
/// <param name="userID"></param>
public TaskManager(int userID)
{
this.userID = userID;
}
/// <summary>
/// Get a task list of one category for the current user
/// </summary>
public List<Task> GetTaskSet(int categoryID)
{
return ctx.TaskSet.Include(x => x.SubTaskSet).Where(x => x.Category.UserID == this.userID && x.CategoryID == categoryID).ToList();
}
/// <summary>
/// Get a task including its subtasks
/// </summary>
public Task GetTask(int taskID)
{
var t = ctx.TaskSet.Include(x => x.SubTaskSet).Where(x => x.Category.UserID == this.userID && x.TaskID == taskID).SingleOrDefault();
return t;
}
/// <summary>
/// Create a new task from Task object
/// </summary>
public Task CreateTask(Task t)
{
ValidateTask(t);
return this.New(t);
}
/// <summary>
/// Create a new task from details
/// </summary>
public Task CreateTask(int categoryID, string title, string note, DateTime due, Importance importance, decimal? effort, List<SubTask> subtasks = null)
{
this.StartTracking();
var t = new Task();
t.CategoryID = categoryID;
t.Created = DateTime.Now;
SetTaskDetails(t, title, note, due, importance, false, effort, subtasks);
this.New(t);
this.SetTracking();
return t;
}
private static void SetTaskDetails(Task t, string title, string note, DateTime? due, Importance? importance, bool done, decimal? effort, List<SubTask> subtasks)
{
t.Title = title;
t.Note = note;
t.Due = due;
t.Importance = importance;
t.SubTaskSet = subtasks;
t.Effort = effort;
t.Done = done;
}
/// <summary>
/// Change a task
/// </summary>
public Task ChangeTask(int taskID, string title, string note, DateTime due, Importance? importance, bool done, decimal? effort, List<SubTask> subtasks)
{
ctx = new Context();
ctx.Log();
// Delete subtasks and then create new ones instead of change detection!
stm.DeleteSubTasks(taskID);
var t = ctx.TaskSet.SingleOrDefault(x => x.TaskID == taskID);
SetTaskDetails(t, title, note, due, importance, done, effort, null);
ctx.SaveChanges();
t.SubTaskSet = subtasks;
ctx.SaveChanges();
return t;
}
public void Log(string s)
{
Debug.WriteLine(s);
}
/// <summary>
/// Change a task including subtasks
/// </summary>
public Task ChangeTask(Task tnew)
{
if (tnew == null) return null;
// Validate of the sent data!
if (tnew.Category != null) tnew.Category = null; // user cannot change the category this way!
ValidateTask(tnew);
var ctx1 = new Context();
ctx1.Log(Log);
stm.DeleteSubTasks(tnew.TaskID);
if (tnew.SubTaskSet != null) tnew.SubTaskSet.ForEach(x => x.SubTaskID = 0); // delete ID, so that EFCore regards this as a new object
tnew.CategoryID = this.GetByID(tnew.TaskID).CategoryID; // Use existing category
ctx1.TaskSet.Update(tnew);
var count = ctx1.SaveChanges();
return tnew;
}
/// <summary>
/// Checks if the TaskID exists and belongs to the current user
/// </summary>
private void ValidateTask(int taskID)
{
var taskAusDB = ctx.TaskSet.Include(t => t.Category).SingleOrDefault(x => x.TaskID == taskID);
if (taskAusDB == null) throw new UnauthorizedAccessException("Task nicht vorhanden!");
if (taskAusDB.Category.UserID != this.userID) throw new UnauthorizedAccessException("Task gehört nicht zu diesem User!");
}
/// <summary>
/// Checks if transferred task object is valid
/// </summary>
private void ValidateTask(Task tnew = null)
{
ValidateTask(tnew.TaskID);
if (tnew.CategoryID > 0)
{
var catAusDB = new CategoryManager(this.userID).GetByID(tnew.CategoryID);
if (catAusDB.UserID != this.userID) throw new UnauthorizedAccessException("Task gehört nicht zu diesem User!");
}
}
/// <summary>
/// Full-text search in tasks and subtasks, return tasks grouped by category
/// </summary>
public List<Category> Search(string text)
{
var r = new List<Category>();
text = text.ToLower();
var taskSet = ctx.TaskSet.Include(x => x.SubTaskSet).Include(x => x.Category).
Where(x => x.Category.UserID == this.userID && // nur von diesem User !!!
(x.Title.ToLower().Contains(text) || x.Note.ToLower().Contains(text) || x.SubTaskSet.Any(y => y.Title.Contains(text)))).ToList();
foreach (var t in taskSet)
{
if (!r.Any(x => x.CategoryID == t.CategoryID)) r.Add(t.Category);
}
return r;
}
/// <summary>
/// Returns all tasks due, including tomorrow, grouped by category, sorted by date
/// </summary>
public List<Category> GetDueTaskSet()
{
var tomorrow = DateTime.Now.Date.AddDays(1);
var r = new List<Category>();
var taskSet = ctx.TaskSet.Include(x => x.SubTaskSet).Include(x => x.Category).
Where(x => x.Category.UserID == this.userID && // nur von diesem User !!!
(x.Done == false && x.Due != null && x.Due.Value.Date <= tomorrow)).OrderByDescending(x => x.Due).ToList();
foreach (var t in taskSet)
{
if (!r.Any(x => x.CategoryID == t.CategoryID)) r.Add(t.Category);
}
return r;
}
/// <summary>
/// Remove Task with its subtasks
/// </summary>
public void RemoveTask(int id)
{
ValidateTask(id);
this.Remove(id);
}
}
}
Listing A-7
TaskManager.cs
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace ITVisions.EFC
{
/// <summary>
/// Base class for all data managers to manage a specific entity type, even if they are detached
/// V1.3
/// Assumption: There is always only one primary key column!
/// </summary>
public abstract class EntityManagerBase<TDbContext, TEntity> : DataManagerBase<TDbContext>
where TDbContext : DbContext, new()
where TEntity : class
{
public EntityManagerBase() : base(false)
{
}
public EntityManagerBase(bool tracking) : base(tracking)
{
}
protected EntityManagerBase(TDbContext kontext = null, bool tracking = false) : base(kontext, tracking)
{
}
/// <summary>
/// Get object based on the primary key
/// </summary>
/// <returns></returns>
public virtual TEntity GetByID(object id)
{
return ctx.Set<TEntity>().Find(id);
}
/// <summary>
/// Saves changed object
/// </summary>
public TEntity Update(TEntity obj)
{
if (!this.tracking) this.StartTracking(); // Start change tracking if no-tracking is on
ctx.Set<TEntity>().Attach(obj);
ctx.Entry(obj).State = EntityState.Modified;
ctx.SaveChanges();
this.SetTracking();
return obj;
}
/// <summary>
/// Adds a new object
/// </summary>
public TEntity New(TEntity obj)
{
if (!this.tracking) this.StartTracking(); // Start change tracking if no-tracking is on
ctx.Set<TEntity>().Add(obj);
ctx.SaveChanges();
this.SetTracking();
return obj;
}
/// <summary>
/// Deletes an object based on the primary key
/// </summary>
public virtual void Remove(object id)
{
if (!this.tracking) this.StartTracking(); // Start change tracking if no-tracking is on
TEntity obj = ctx.Set<TEntity>().Find(id);
Remove(obj);
this.SetTracking();
}
/// <summary>
/// Deletes an object
/// </summary>
public bool Remove(TEntity obj)
{
if (!this.tracking) this.StartTracking(); // Switch on tracking for a short time
if (!this.IsLoaded(obj)) ctx.Set<TEntity>().Attach(obj);
ctx.Set<TEntity>().Remove(obj);
ctx.SaveChanges();
this.SetTracking();
return true;
}
/// <summary>
/// Checks if an object is already in the local cache
/// </summary>
public bool IsLoaded(TEntity obj)
{
return ctx.Set<TEntity>().Local.Any(e => e == obj);
}
}
}
Listing A-8
EntityBaseManager.cs
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace ITVisions.EFC
{
/// <summary>
/// Base class for all data managers
/// </summary>
abstract public class DataManagerBase<TDbContext> : IDisposable
where TDbContext : DbContext, new()
{
// One instance of the framework context per manager instance
protected TDbContext ctx;
protected bool disposeContext = true;
protected bool tracking = false;
protected DataManagerBase(bool tracking) : this(null, tracking)
{
}
public void StartTracking()
{
ctx.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.TrackAll;
}
public void SetTracking()
{
if (tracking) ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
else ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DataManagerBase()
{
this.ctx = new TDbContext();
}
protected DataManagerBase(TDbContext kontext = null, bool tracking = false)
{
this.tracking = tracking;
// If a context has been handed in, take this!
if (kontext != null) { this.ctx = kontext; disposeContext = false; }
else
{
this.ctx = new TDbContext();
}
SetTracking();
}
/// <summary>
/// Destroy DataManager (also destroys the EF context)
/// </summary>
public void Dispose()
{
// If the context was submitted from the outside, we should not call Dispose() on the context! That's up to the caller!
if (disposeContext) ctx.Dispose();
}
/// <summary>
/// Save Changes in context
/// </summary>
/// <returns>a string that contains information about the number of new, changed, and deleted records</returns>
public string Save()
{
string ergebnis = GetChangeTrackerStatistics();
var count = ctx.SaveChanges();
return ergebnis;
}
/// <summary>
/// Save for detached entity objects with auto increment primary key named ID
// The newly added objects must return the store routine because the IDs for the
/// </summary>
protected List<TEntity> Save<TEntity>(IEnumerable<TEntity> menge, out string Statistik)
where TEntity : class
{
StartTracking();
var newObjects = new List<TEntity>();
foreach (dynamic o in menge)
{
// Attach to the context
ctx.Set<TEntity>().Attach((TEntity)o);
if (o.ID == 0) // No value -> new object
{
ctx.Entry(o).State = EntityState.Added;
if (o.ID < 0) o.ID = 0; // Necessary hack, because EFCore writes a big negative number in ID after the added and considers that as key :-(
// Remember new records because they have to be returned after saving (they will have their IDs!)
newObjects.Add(o);
}
else // existing object --> UPDATE
{
ctx.Entry(o).State = EntityState.Modified;
}
SetTracking();
}
// Get statistics of changes
Statistik = GetChangeTrackerStatistics<TEntity>();
var e = ctx.SaveChanges();
return newObjects;
}
/// <summary>
/// Save for detached entity objects with an EntityState property
/// </summary>
protected List<TEntity> SaveEx<TEntity>(IEnumerable<TEntity> menge, out string Statistik)
where TEntity : class
{
StartTracking();
var newObjects = new List<TEntity>();
foreach (dynamic o in menge)
{
if (o.EntityState == ITVEntityState.Added)
{
ctx.Entry(o).State = EntityState.Added;
newObjects.Add(o);
}
if (o.EntityState == ITVEntityState.Deleted)
{
ctx.Set<TEntity>().Attach((TEntity)o);
ctx.Set<TEntity>().Remove(o);
}
if (o.EntityState == ITVEntityState.Modified)
{
ctx.Set<TEntity>().Attach((TEntity)o);
ctx.Entry(o).State = EntityState.Modified;
}
}
Statistik = GetChangeTrackerStatistics<TEntity>();
ctx.SaveChanges();
SetTracking();
return newObjects;
}
/// <summary>
/// Provides statistics from the ChangeTracker as a string
/// </summary>
protected string GetChangeTrackerStatistics<TEntity>()
where TEntity : class
{
string Statistik = "";
Statistik += "Changed: " + ctx.ChangeTracker.Entries<TEntity>().Where(x => x.State == EntityState.Modified).Count();
Statistik += " New: " + ctx.ChangeTracker.Entries<TEntity>().Where(x => x.State == EntityState.Added).Count();
Statistik += " Deleted: " + ctx.ChangeTracker.Entries<TEntity>().Where(x => x.State == EntityState.Deleted).Count();
return Statistik;
}
/// <summary>
/// Provides statistics from the ChangeTracker as a string
/// </summary>
protected string GetChangeTrackerStatistics()
{
string Statistik = "";
Statistik += "Changed: " + ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified).Count();
Statistik += " New: " + ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Added).Count();
Statistik += " Deleted: " + ctx.ChangeTracker.Entries().Where(x => x.State == EntityState.Deleted).Count();
return Statistik;
}
}
}
Listing A-9
DataManagerBase.cs
Web API
MiracleList 后端提供了两个版本的基于 HTTPS 的 REST 服务。
- 在 REST 服务的版本 1 中,身份验证令牌在 URL 中传递。
- 在 REST 服务的版本 2 中,身份验证令牌在 HTTP 头中传递。
REST 服务的版本 1 提供了以下操作:
POST/Login
:使用客户端 ID、用户名和密码登录。此操作/登录发回一个 GUID 作为会话令牌,将在所有后续操作中给出。GET/Logoff/{token}
:注销用户。GET/CategorySet/{token}
:列表类别。GET/TaskSet/{token}/{id}
:分类列出任务。GET/Task/{token}/{id}
:列出子任务的详细信息。GET/Search/{token}/{text}
:任务和子任务中的全文搜索。GET/DueTaskSet/{token}
:列出到期任务。POST/CreateCategory/{token}/{name}
:创建类别。POST/CreateTask/{token}
:创建一个任务以 JSON 格式提交到主体中(包括子任务)。PUT/ChangeTask/{token}
:修改 JSON 格式的主体中要提交的任务(包括子任务)。DELETE/DeleteTask/{token}/{id}
:删除带有所有子任务的任务。DELETE/DeleteCategory/{token}/{id}
:删除包含所有任务和子任务的类别。
REST 服务的第 2 版还提供了以下操作:
POST/Login
:使用客户端 ID、用户名和密码登录。此操作返回一个 GUID 作为会话令牌,包含在所有后续操作中。GET/CategorySet/
:列表类别。GET/TaskSet/{id}
:分类列出任务。GET/Task/{id}
:列出带有子任务的任务的详细信息。POST/CreateCategory/{name}
:创建类别。POST/CreateTask and PUT/ChangeTask
:创建或更改 JSON 格式的主体中要提交的任务(包括子任务)。
对于所有 REST 操作,RESTful APIs 的 Swagger OpenAPI 规范中提供了元数据( http://swagger.io
)。参见 https://miraclelistbackend.azurewebsites.net/swagger/v1/swagger.json
获取 REST 服务的正式描述,参见 https://miraclelistbackend.azurewebsites.net/swagger
获取适当的帮助页面。后端还支持跨源资源共享(CORS),以允许访问任何其他托管的网站( https://www.w3.org/TR/cors
)。
在/Login
指定的客户端 ID 必须由每个客户端开发者在 https://miraclelistbackend.azurewebsites.net/Client
请求一次。每个客户端 ID 的任务数量限制为 1,000。另一方面,不需要在后端创建用户帐户。由于这是一个示例应用,如果您提交的用户不存在,将自动创建一个用户。每个新帐户自动有三个类别(工作、家庭和休闲),并带有示例任务,如安排团队会议、倒垃圾和为山地车马拉松训练。
清单 A-10 显示了 ASP.NET 核心应用的启动代码,它启用并配置各种组件,如下所示:
- ASP.NET 核心 MVC。
- CORS 允许任何 web 客户端访问 web API。
- 监控和遥测数据的应用洞察。Application Insights 是微软提供的云服务。
- 禁用 JSON 序列化程序中循环引用的序列化。(循环引用在 JSON 中不是标准化的。有社区解决方案,但是如果您不需要依赖这些社区解决方案,您应该避免这样做。)
- Swagger 为 REST 操作创建了 OpenAPI 规范和帮助页面。
using BL;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.PlatformAbstractions;
using Swashbuckle.AspNetCore.Swagger;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Miraclelist
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
// Connect to EFCore Profiler
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
// inject connection string into DAL
DAL.Context.ConnectionString = Configuration.GetConnectionString("MiracleListDB");
#region testuser
if (env.IsEnvironment("Development"))
{
var um2 = new UserManager("unittest", "unittest");
um2.InitDefaultTasks();
}
#endregion
}
public IConfigurationRoot Configuration { get; }
/// <summary>
/// Called by ASP.NET Core during startup
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
#region Enable Auth service for MLToken in the HTTP header
services.AddAuthentication().AddMLToken();
#endregion
#region Enable App Insights
services.AddApplicationInsightsTelemetry(Configuration);
#endregion
#region JSON configuration: no circular references and ISO date format
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
options.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None;
options.SerializerSettings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.IsoDateFormat;
});
#endregion
#region Enable MVC
services.AddMvc(options =>
{
// Exception Filter
options.Filters.Add(typeof(GlobalExceptionFilter));
//options.Filters.Add(typeof(GlobalExceptionAsyncFilter));
options.Filters.Add(typeof(LoggingActionFilter));
});
#endregion
#region Enable CORS
services.AddCors();
#endregion
// Make configuration available everywhere
services.AddSingleton(Configuration);
#region Swagger
services.AddSwaggerGen(c =>
{
c.DescribeAllEnumsAsStrings(); // Important for Enums!
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "MiracleList API",
Description = "Backend for MiracleList.de with token in URL",
TermsOfService = "None",
Contact = new Contact { Name = "Holger Schwichtenberg", Email = "", Url = "http://it-visions.de/kontakt" }
});
c.SwaggerDoc("v2", new Info
{
Version = "v2",
Title = "MiracleList API",
Description = "Backend for MiracleList.de with token in HTTP header",
TermsOfService = "None",
Contact = new Contact { Name = "Holger Schwichtenberg", Email = "", Url = "http://it-visions.de/kontakt" }
});
// Adds tokens as header parameters
c.OperationFilter<SwaggerTokenHeaderParameter>();
// include XML comments in Swagger doc
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "Miraclelist_WebAPI.xml");
c.IncludeXmlComments(xmlPath);
});
#endregion
}
/// <summary>
/// Called by ASP.NET Core during startup
/// </summary>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
#region Error handling
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
await context.Response.WriteAsync("ASP.NET Core Exception Middleware:" + ex.ToString());
}
});
});
// ---------------------------- letzte Fehlerbehandlung: Fehlerseite für HTTP-Statuscode
app.UseStatusCodePages();
#endregion
#region ASP.NET Core services
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseDirectoryBrowser();
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
#endregion
#region CORS
// NUGET: install-Package Microsoft.AspNet.Cors
// Namespace: using Microsoft.AspNet.Cors;
app.UseCors(builder =>
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
#endregion
#region Swagger
// NUGET: Install-Package Swashbuckle.AspNetCore
// Namespace: using Swashbuckle.AspNetCore.Swagger;
app.UseSwagger(c =>
{
});
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "MiracleList v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "MiracleList v2");
});
#endregion
#region MVC with Routing
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
#endregion
}
}
public class GlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is UnauthorizedAccessException)
{
context.HttpContext.Response.StatusCode = 403;
}
else
{
context.HttpContext.Response.StatusCode = 500;
}
context.HttpContext.Response.ContentType = "text/plain";
context.HttpContext.Response.WriteAsync("GlobalExceptionFilter:" + context.Exception.ToString());
}
}
public class GlobalExceptionAsyncFilter : IAsyncExceptionFilter
{
public Task OnExceptionAsync(ExceptionContext context)
{
context.HttpContext.Response.StatusCode = 500;
context.HttpContext.Response.ContentType = "text/plain";
return context.HttpContext.Response.WriteAsync("MVC GlobalExceptionAsyncFilter:" + context.Exception.ToString());
}
}
}
Listing A-10
Startup.cs
清单 A-11 展示了 REST 服务版本 1 中WebAPI
控制器的实现,包括使用应用洞察收集遥测数据。WebAPI
控制器完全没有数据访问代码。因此,这里没有使用实体框架核心。所有数据操作都封装在业务逻辑层中。WebAPI
控制器只使用在那里实现的管理器类。
using BL;
using BO;
using ITVisions;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Miraclelist.Controllers
{
/// <summary>
/// DTO
/// </summary>
public class LoginInfo
{
public string ClientID;
public string Username;
public string Password;
public string Token;
public string Message;
}
/// <summary>
/// API v1
/// </summary>
[Route("")]
[ApiExplorerSettings(GroupName = "v1")]
public class MiracleListApiController : Controller
{
private TelemetryClient telemetry = new TelemetryClient();
TaskManager tm;
UserManager um;
CategoryManager cm;
public MiracleListApiController()
{
}
/// <summary>
/// Helper for all actions to check the token and save telemetry data
/// </summary>
private bool CheckToken(string token, [CallerMemberName] string caller = "?")
{
if (token == null || token.Length < 2)
{
// save telemetry data
var p2 = new Dictionary<string, string>();
p2.Add("token", token);
telemetry.TrackEvent("TOKENERROR_" + caller, p2);
new LogManager().Log(Event.TokenCheckError, Severity.Warning, "Ungültiges Token", caller, token);
throw new Exception("Ungültiges Token!");
}
// validate tokne
um = new UserManager(token);
var checkResult = um.IsValid();
if (checkResult != UserManager.TokenValidationResult.Ok)
{
// save telemetry data
var p2 = new Dictionary<string, string>();
p2.Add("token", token);
p2.Add("checkResult", checkResult.ToString());
telemetry.TrackEvent("USERERROR_" + caller, p2);
new LogManager().Log(Event.TokenCheckError, Severity.Warning, checkResult.ToString(), caller, token, um.CurrentUser?.UserID);
throw new Exception(checkResult.ToString());
}
um.InitDefaultTasks();
// Create manager objects
cm = new CategoryManager(um.CurrentUser.UserID);
tm = new TaskManager(um.CurrentUser.UserID);
// save telemetry data
var p = new Dictionary<string, string>();
p.Add("token", token);
p.Add("user", um.CurrentUser.UserName);
telemetry.TrackEvent(caller, p);
new LogManager().Log(Event.TokenCheckOK, Severity.Information, null, caller, token, um.CurrentUser?.UserID);
return true;
}
/// <summary>
/// About this server
/// </summary>
/// <returns></returns>
[Route("/About")]
[HttpGet]
public IEnumerable<string> About()
{
return new AppManager().GetAppInfo().Append("API-Version: v1");
}
/// <summary>
/// Get version of server
/// </summary>
/// <returns></returns>
[Route("/Version")]
[HttpGet]
public string Version()
{
return
Assembly.GetEntryAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion.ToString();
}
/// <summary>
/// Nur für einen Test
/// </summary>
/// <returns></returns>
[Route("/About2")]
[ApiExplorerSettings(IgnoreApi = true)]
[HttpGet]
public JsonResult GetAbout2()
{
var v = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
var e = new string[] { "MiracleListBackend", "(C) Dr. Holger Schwichtenberg, www.IT-Visions.de", "Version: " + v };
var r = new JsonResult(e);
this.Response.Headers.Add("X-Version", v);
r.StatusCode = 202;
return r;
}
/// <summary>
/// Login with a client ID, username and password. This operation sends back a GUID as a session token, to be used in all following operations.
/// </summary>
[HttpPost("Login")] // neu
public async System.Threading.Tasks.Task<LoginInfo> Login([FromBody] LoginInfo loginInfo)
{
if (string.IsNullOrEmpty(loginInfo.Password))
{
new LogManager().Log(Event.LogginError, Severity.Warning, "", "password empty");
throw new Exception("ERROR: password empty!");
}
var cm = new ClientManager();
var e = cm.CheckClient(loginInfo.ClientID);
if (e.CheckClientResultCode != ClientManager.CheckClientResultCode.Ok)
{
new LogManager().Log(Event.LogginError, Severity.Warning, Enum.GetName(typeof(ClientManager.CheckClientResultCode), e.CheckClientResultCode) + "\n" + e.client?.ToNameValueString(), "ClientIDCheck", "", um?.CurrentUser?.UserID);
return new LoginInfo()
{
Message = "Client-ID-Check: " + Enum.GetName(typeof(ClientManager.CheckClientResultCode), e.CheckClientResultCode)
};
}
var u = new UserManager(loginInfo.Username, loginInfo.Password).CurrentUser;
if (u == null)
{
new LogManager().Log(Event.LogginError, Severity.Warning, loginInfo.ToNameValueString() + "\n" + e.client?.ToNameValueString(), "UserCheck", u?.Token, um?.CurrentUser?.UserID);
return new LoginInfo() { Message = "Access denied!" };
}
loginInfo.Token = u.Token;
new LogManager().Log(Event.LoginOK, Severity.Information, null, "UserCheck", u.Token, u.UserID);
loginInfo.Password = "";
return loginInfo;
}
/// <summary>
/// Delete token
/// </summary>
[HttpGet("Logoff/{token}")]
public bool Logoff(string token)
{
return UserManager.Logoff(token);
}
/// <summary>
/// Get a list of all categories
/// </summary>
[HttpGet("CategorySet/{token}")]
public IEnumerable<Category> GetCategorySet(string token)
{
if (!CheckToken(token)) return null;
return cm.GetCategorySet();
}
/// <summary>
/// Get a list of tasks in one category
/// </summary>
[HttpGet("TaskSet/{token}/{id}")]
public IEnumerable<Task> GetTaskSet(string token, int id)
{
if (id <= 0) throw new Exception("Invalid ID!");
if (!CheckToken(token)) return null;
return tm.GetTaskSet(id);
}
/// <summary>
/// Get details of one task
/// </summary>
[HttpGet("Task/{token}/{id}")]
public Task Task(string token, int id)
{
if (id <= 0) throw new Exception("Invalid ID!");
if (!CheckToken(token)) return null;
return tm.GetTask(id);
}
/// <summary>
/// Search in tasks and subtasks
/// </summary>
[HttpGet("Search/{token}/{text}")]
public IEnumerable<Category> Search(string token, string text)
{
if (!CheckToken(token)) return null;
return tm.Search(text);
}
/// <summary>
/// Returns all tasks due, including tomorrow, grouped by category, sorted by date
/// </summary>
[HttpGet("DueTaskSet/{token}")]
public IEnumerable<Category> GetDueTaskSet(string token)
{
if (!CheckToken(token)) return null;
return tm.GetDueTaskSet();
}
/// <summary>
/// Create a new category
/// </summary>
[HttpPost("CreateCategory/{token}/{name}")]
public Category CreateCategory(string token, string name)
{
if (!CheckToken(token)) return null;
return cm.CreateCategory(name);
}
/// <summary>
/// Create a task to be submitted in body in JSON format (including subtasks)
/// </summary>
/// <param name="token"></param>
/// <param name="t"></param>
/// <returns></returns>
[HttpPost("CreateTask/{token}")] // neu
public Task CreateTask(string token, [FromBody]Task t)
{
if (!CheckToken(token)) return null;
return tm.New(t);
}
/// <summary>
/// Create a task to be submitted in body in JSON format (including subtasks)
/// </summary>
[HttpPut("ChangeTask/{token}")] // geändert
public Task ChangeTask(string token, [FromBody]Task t)
{
if (!CheckToken(token)) return null;
return tm.ChangeTask(t);
}
/// <summary>
/// Set a task to "done"
/// </summary>
[HttpPut("ChangeTaskDone/{token}")]
public Task ChangeTaskDone(string token, int id, bool done)
{
throw new UnauthorizedAccessException("du kommst hier nicht rein!");
}
/// <summary>
/// Change a subtask
/// </summary>
[HttpPut("ChangeSubTask/{token}")]
public SubTask ChangeSubTask(string token, [FromBody]SubTask st)
{
throw new UnauthorizedAccessException("du kommst hier nicht rein!");
}
/// <summary>
/// Delete a task with all subtasks
/// </summary>
[HttpDelete("DeleteTask/{token}/{id}")]
public void DeleteTask(string token, int id)
{
if (!CheckToken(token)) return;
tm.RemoveTask(id);
}
/// <summary>
/// Delete a category with all tasks and subtasks
/// </summary>
[HttpDelete("[action]/{token}/{id}")]
public void DeleteCategory(string token, int id)
{
if (!CheckToken(token)) return;
cm.RemoveCategory(id);
}
}
}
Listing A-11MiracleListApiController.cs (Version 1 of the REST Service)
通过依赖注入使用实体框架核心
当在 Visual Studio 中使用“个人用户帐户”或“在应用中存储用户帐户”选项创建新的 ASP.NET 核心应用时,实体框架核心还会创建一个实体框架核心上下文(ApplicationDbContext
),该上下文由基类Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<T> inherits
创建,并使用类ApplicationUser
作为类型参数,类型参数又由Microsoft.AspNetCore.Identity.IdentityUser inherits
提供。如果需要的话,ApplicationUser
类可以扩展。此外,还会创建一个模式迁移。连接字符串存储在appsettings.json
中。它指向一个本地数据库(Microsoft SQL Server LocalDB
)。
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-ASPNETCore20-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
如有必要,您可以更改连接字符串。在一个Update-Database
命令之后,数据库被创建来管理本地用户(图 A-7 )。
图 A-7
Created database for managing users of the ASP.NET Core web application
web 应用中没有上下文类ApplicationDbContext
的实例化。相反,这是由依赖注入使用的。在Startup.cs
中,你会发现两行(见清单 A-12 )。
AddDbContext()
扩展方法将上下文类注册为依赖注入服务,并传递提供者和连接字符串。方法AddDbContext()
由微软类Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions
中的Microsoft.EntityFrameworkCore.dll
提供。AddEntityFrameworkStores()
扩展方法告诉 ASP.NET 身份组件使用哪个上下文类。AddEntityFrameworkStores()
由微软Extensions.Dependency injection.IdentityEntityFrameworkBuilderExtensions
类中的Microsoft.AspNetCore.Identity.EntityFrameworkCore.dll
提供。
Note
ASP.NET 标识确保上下文类在需要时被实例化,此外,它的生存期不会超过处理一个 HTTP 请求所需的时间。
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
Listing A-12An Extract from Startup.cs, an ASP.NET Core Web Application Created with the Project Template in Visual Studio, with Individual Local User Accounts
Tip
您也可以将AddDbContext()
用于您自己的上下文类。在这种情况下,使用AddDbContext()
在启动类中注册上下文类,如下所示:
services.AddDbContext<EFCore_Kontext.WWWingsContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("WWWingsConnection")));
当使用 ASP.NET 核心(Microsoft.Extensions.DependencyInjection
)的标准依赖注入组件时,依赖注入只能通过构造函数注入来完成(参见清单 A-13 )。
using GO;
using EFCore_Context;
using System.Collections.Generic;
using System.Linq;
namespace ASPNETCore_NETCore.BL
{
public class FlightManager
{
private WWWingsContext ctx;
/// <summary>
/// constructor
/// </ summary>
/// <param name="ctx">context instance comes via DI!</ Param>
public FlightManager(WWWingsContext ctx)
{
this.ctx = ctx;
}
public List<Flight> GetFlightSet string departure, int from, int to)
{
var FlightSet = ctx.Flight
.Where(x => x.Departure == departure)
.Skip(from).Take(to - from).ToList();
return FlightSet.ToList();
}
}
}
Listing A-13The Class FlightManager Receives the Context Instance via Dependency Injection
但是,请注意,如果FlightManager
类的实例本身是由依赖注入(DI)容器生成的,那么 ASP.NET 核心只将上下文实例注入到FlightManager
类的构造函数中。为此,类FlightManager
必须在启动类中注册,以便用AddTransient()
进行依赖注入。
ServicesAddTransient <Flight manager>();
每次请求实例时,AddTransient()
都会生成一个新的FlightManager
实例。AddScoped()
将确保相同的实例总是作为 HTTP 请求的一部分返回;这可能是所希望的,因为实体框架核心上下文的高速缓存被填充。AddSingleton()
将总是跨多个 HTTP 请求提供相同的实例。这无法工作,因为实体框架核心上下文不支持多线程。
然后,一个 ASP.NET MVC 控制器通过构造函数注入期待一个FlightManager
的实例(清单 A-14 )。
public class WWWingsController: Controller
{
private FlightManager fm;
public WWWingsController(FlightManager fm)
{
this.fm = fm;
}
...
}
Listing A-14The WWWingsController Class Receives an Instance of FlightManager via Dependency Injection
实际例子:上下文实例池(DbContext Pooling)
自从实体框架 Core 2.0 以后,就可以用AddDbContextPool()
代替AddDbContext()
。该方法创建一组在池中管理的上下文实例,类似于 ADO.NET 连接池。当依赖注入请求一个上下文实例时,从池中取出一个空闲的上下文实例。实体框架核心还可以重置已经使用的上下文实例,并释放它以供重用。这在一定程度上提高了使用实体框架核心的 web 应用的性能。
int poolSize = 40;
services.AddDbContextPool<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), poolSize);
在通用 Windows 平台应用中使用实体框架核心
本节中使用的 MiracleList Light 应用是一个用于简单本地任务管理的示例应用,实现为 Windows 10 的通用 Windows 平台(UWP)应用,使用本地 SQLite 数据库作为数据存储,在云中没有后端(图 A-8 )。这个应用允许你保存在特定日期到期的任务。在软件的当前版本中,任务总是正好有三个子任务(计划、执行、回顾)不能被改变。通过单击完成或全部移除,可以从列表中移除任务。
Note
要在 UWP 中使用实体框架核心 2.0,您需要在 Windows 10 Creators 2017 秋季更新中安装 UWP 版本 10.0.16299。
图 A-8
The MiracleList Light app for Windows 10
架构
由于代码大小有限,该应用在 Visual Studio 项目中作为一个整体应用来实现。该项目是使用项目模板 Windows 通用/空白应用创建的。对于此模板,Windows 10 SDK 必须安装在适合您的 Windows 安装的版本中。Microsoft 不支持在旧操作系统上使用或编译程序。
应用引用了 NuGet 包Microsoft.EntityFrameworkCore.Sqlite
。
该应用对数据库使用正向工程。如有必要,如果应用启动时数据库文件不存在,则会在运行时生成具有适当数据库模式的数据库文件。图 A-9 显示了项目的结构。
图 A-9
Structure of the project
实体
只需要两个实体类。
- 任务的任务类:一个
Task
对象在Details
属性中有一个List<TaskDetail>
。 - 子任务的 TaskDetail 类:每个
TaskDetail
对象使用Task
属性指向子任务所属的任务。此外,TaskID
外键属性中的TaskDetail
类知道父任务的主键。
在这种情况下,实体类的数据注释是不必要的,因为实体框架核心可以完全基于内置约定来创建数据库模式。图 A-10 展示了应用的对象模型,清单 A-15 展示了实现。
图 A-10
Object model of the application
using System;
using System.Collections.Generic;
namespace EFC_UWP_SQLite
{
/// <summary>
/// Entity class for tasks
/// </summary>
public class Task
{
// Basic properties
public int TaskID { get; set; } // PK
public string Title { get; set; } // TEXT
public DateTime Date { get; set; } // DateTime
// Navigation properties
public List<TaskDetail> Details { get; set; } = new List<TaskDetail>();
public string View { get { return Date.ToString("d") + ": " + Title; } }
}
/// <summary>
/// Entity class for subtasks
/// </summary>
public class TaskDetail
{
// Basic properties
public int TaskDetailID { get; set; } // PK
public string Text { get; set; }
// Navigation properties
public Task Task { get; set; }
public int TaskID { get; set; } // optional: Foreign key column for navigation relationship
}
}
Listing A-15Implementation of the Two Entity Classes in the EntityClasses.cs File
实体框架核心上下文类
对于两个实体类,从DbContext
派生的上下文类总是有一个类型为DbSet<T>
的属性。在OnConfiguring()
方法中,UseSqlite()
设置实体框架核心数据库提供者,只将 SQLite 数据库文件的名称作为参数传入。在这种情况下,OnModelCreating()
的实现是不必要的,因为实体框架核心可以完全基于内置约定创建数据库模式(清单 A-16 )。
using Microsoft.EntityFrameworkCore;
namespace EFC_UWP_SQLite
{
/// <summary>
/// Entity Framework core context
/// </summary>
public class EFContext : DbContext
{
public static string FileName = "MiracleList.db";
public DbSet<Task> TaskSet { get; set; }
public DbSet<TaskDetail> TaskDetailSet { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Set provider and database filename
optionsBuilder.UseSqlite($"Filename={FileName}");
}
}
}
Listing A-16Implementation of the Context Class in the File EFContext.cs
起动电码
当应用启动时,App.xaml.cs
数据库文件使用Database
方法。如果数据库文件尚不存在,则创建EnsureCreated()
。英文源代码注释来自微软的项目模板。参见清单 A-17 。
using System;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace EFC_UWP_SQLite
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
// Create DB, if not exists!
using (var db = new EFContext())
{
db.Database.EnsureCreated();
}
}
...
}
}
Listing A-17Extract from the App.xaml.cs File
生成的数据库
生成的数据库可以与 SQLite 的工具数据库浏览器交互显示和使用,如图 A-11 、图 A-12 和图 A-13 所示。
图 A-13
Executing SQL commands in DB Browser for SQLite
图 A-12
Data view in DB Browser for SQLite
图 A-11
Database schema view in DB Browser for SQLite
| | ![A461790_1_En_21_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/mod-data-access-ef-core/img/A461790_1_En_21_Figa_HTML.jpg) | | 工具名称 | SQLite 的数据库浏览器 | | 网站 | [`www.sqlitebrowser.org`](http://www.sqlitebrowser.org) | | 免费版本 | 是 | | 商业版 | 不 |数据访问代码
在这个简单的案例研究中,数据访问代码没有从表示层中分离出来。它还故意不使用模型-视图-视图模型(MVVM)模式,这将使本书中的程序代码易于管理。
数据访问使用实体框架核心的异步方法来保持 UI 的响应性。
当创建子任务和删除所有任务时,会显示两个变量(清单 A-18 )。被注释掉的变量是效率较低的变量。因此,在不使用 SQL 的情况下删除所有任务和子任务需要不必要地加载所有任务并为每个任务发送一个DELETE
命令。在这两种情况下,子任务的显式删除都不是必需的,因为级联删除在标准系统中是有效的。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Windows.Foundation;
using Windows.Storage;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
namespace EFC_UWP_SQLite
{
/// <summary>
/// Main page of the app
/// </summary>
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public MainPage()
{
this.DataContext = this;
this.InitializeComponent();
Windows.UI.ViewManagement.ApplicationView.PreferredLaunchViewSize = new Size(800, 500);
Windows.UI.ViewManagement.ApplicationView.PreferredLaunchWindowingMode = Windows.UI.ViewManagement.ApplicationViewWindowingMode.PreferredLaunchViewSize;
System.Diagnostics.Debug.WriteLine(ApplicationData.Current.LocalFolder.Path);
}
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Task> _Tasks { get; set; }
public ObservableCollection<Task> Tasks
{
get { return _Tasks; }
set { _Tasks = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Tasks))); }
}
private string _Statustext { get; set; }
public string Statustext
{
get { return _Statustext; }
set { _Statustext = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Statustext))); }
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
var count = await this.LoadTaskSet();
SetStatus(count + " records loaded!");
}
private void SetStatus(string text)
{
string dbstatus;
using (var db = new EFContext())
{
dbstatus = db.TaskSet.Count() + " tasks with " + db.TaskDetailSet.Count() + " task details. " + ApplicationData.Current.LocalFolder.Path + @"\" + EFContext.FileName;
}
Statustext = text + " / Database Status: " + dbstatus + ")";
}
/// <summary>
/// Get all tasks from database
/// </summary>
/// <returns></returns>
private async System.Threading.Tasks.Task<int> LoadTaskSet()
{
using (var db = new EFContext())
{
var list = await db.TaskSet.OrderBy(x => x.Date).ToListAsync();
Tasks = new ObservableCollection<Task>(list);
return Tasks.Count;
}
}
private async void Add(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(C_Task.Text)) return;
if (!C_Date.Date.HasValue) { C_Date.Date = DateTime.Now; }
// Create new Task
var t = new Task { Title = C_Task.Text, Date = C_Date.Date.Value.Date };
var d1 = new TaskDetail() { Text = "Plan" };
var d2 = new TaskDetail() { Text = "Execute" };
var d3 = new TaskDetail() { Text = "Run Retrospective" };
// Alternative 1
//t.Details.Add(d1);
//t.Details.Add(d2);
//t.Details.Add(d3);
// Alternative 2
t.Details.AddRange(new List<TaskDetail>() { d1, d2, d3 });
using (var db = new EFContext())
{
db.TaskSet.Add(t);
// Save now!
var count = await db.SaveChangesAsync();
SetStatus(count + " records saved!");
await this.LoadTaskSet();
}
this.C_Task.Text = "";
this.C_Task.Focus(FocusState.Pointer);
}
private async void SetDone(object sender, RoutedEventArgs e)
{
// Get TaskID
var id = (int)((sender as Button).CommandParameter);
// Remove record
using (var db = new EFContext())
{
Task t = db.TaskSet.SingleOrDefault(x => x.TaskID == id);
if (t == null) return; // not found :-(
db.Remove(t);
var count = db.SaveChangesAsync();
SetStatus(count + " records deleted!");
await this.LoadTaskSet();
}
}
private async void ShowDetails(object sender, RoutedEventArgs e)
{
// Get TaskID
var id = (int)((sender as Button).CommandParameter);
// Get Details
using (var db = new EFContext())
{
string s = "";
Task t = db.TaskSet.Include(x => x.Details).SingleOrDefault(x => x.TaskID == id);
s += "Task: " + t.Title + "\n\n";
s += "Due: " + t.Date.Date + "\n\n";
foreach (var d in t.Details)
{
s += "- " + d.Text + "\n";
}
SetStatus("Details for task #" + id);
await new MessageDialog(s, "Details for task #" + id).ShowAsync();
}
}
private void C_Task_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Enter) Add(null, null);
}
private async void RemoveAll(object sender, RoutedEventArgs e)
{
// Remove all tasks
using (var db = new EFContext())
{
// Alternative 1: unefficient :-(
//foreach (var b in db.TaskSet.ToList())
//{
// db.Remove(b);
//}
//db.SaveChanges();
// Alternative 2: efficient!
//db.Database.ExecuteSqlCommand("Delete from TaskDetailSet");
var count = await db.Database.ExecuteSqlCommandAsync("Delete from TaskSet");
SetStatus(count + " records deleted!");
Tasks = null;
}
}
}
}
Listing A-18Data Access Code in the MainPage.xaml.cs File
用户界面
清单 A-19 显示了 XAML UWP 的应用界面。
<Page
x:Class="EFC_UWP_SQLite.MainPage"
xmlns:="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:EFC_UWP_SQLite"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Loaded="Page_Loaded">
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA1D7E9" Offset="0.081"/>
<GradientStop Color="#FF4C94AD" Offset="0.901"/>
</LinearGradientBrush>
</Grid.Background>
<StackPanel Margin="10,10,10,10" Grid.Row="0">
<!-- ==================== logo -->
<Image x:Name="Logo" Source="Assets/MiracleListLogo.jpg" Width="130" MinHeight="50" HorizontalAlignment="Right"></Image>
<!-- ==================== new Task -->
<TextBlock Text="What do you have to do?" FontSize="20"></TextBlock>
<StackPanel Orientation="horizontal">
<CalendarDatePicker Name="C_Date" />
<TextBox Background="White" Name="C_Task" KeyDown="C_Task_KeyDown" Width="600"></TextBox>
</StackPanel>
<!-- ==================== actions -->
<StackPanel Orientation="horizontal">
<Button Click="Add">Add</Button>
<Button Click="RemoveAll" Margin="10,0,0,0">Remove all</Button>
</StackPanel>
<TextBlock Text="Your task list:" FontSize="20"/>
<!-- ==================== list of tasks -->
<ListView ItemsSource="{Binding Tasks}" ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Background="white" Content="Done" Name="C_Done" CommandParameter="{Binding TaskID}" Click="SetDone" Margin="0,0,10,0" />
<Button Background="white" FontWeight="Bold" Content="{Binding View}" Name="C_Details" CommandParameter="{Binding TaskID}" Click="ShowDetails" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<!-- ==================== statusbar -->
<StackPanel Background="White" Grid.Row="1">
<TextBlock Text="{Binding Statustext}" Margin="10,0,0,0" FontSize="11" />
</StackPanel>
</Grid>
</Page>
Listing A-19MainPage.xaml
在 Xamarin 跨平台应用中使用实体框架核心
本节中的 MiracleList Light 案例研究是上一节中讨论的用于 Windows 10 的通用 Windows 平台(UWP)应用的写照。在这里,它是一款不局限于 Windows 10 作为 UWP 应用的跨平台应用;它也可以在 Android 和 iOS 上运行(图 A-14 和图 A-15 )。对于 GUI,使用Xamarin.Forms
。
Note
对于实体框架核心 2.0,您需要在 Windows 10 Creators 2017 秋季更新中安装 UWP 版本 10.0.16299。
图 A-14
The MiracleList Light cross-platform app for Windows 10
图 A-15
The MiracleList Light cross-platform app for Android
架构
与此应用作为 UWP 应用的实现不同,此跨平台版本是多层的。
- 业务对象项目包含实体类。该项目是一个. NET 标准库,不需要额外的引用。
- DAL 项目包括实体框架核心上下文类。该项目是一个. NET 标准库,需要 NuGet 包
Xamarin.Forms
和Microsoft.EntityFrameworkCore
。 - UI 项目包含使用 Xamarin 窗体的 UI。该项目是一个. NET 标准库,需要 NuGet 包
Xamarin.Forms
和Microsoft.EntityFrameworkCore
。 - 项目
Android
、iOS
和UWP
包含平台特定的启动代码以及应用的平台特定的声明。
该应用对数据库使用正向工程。如有必要,如果应用启动时数据库文件不存在,则会在运行时生成具有适当数据库模式的数据库文件。图 A-16 显示了项目的结构。
图 A-16
Structure of the project
实体
Xamarin 应用的实体类对应于 UWP 案例研究中的实体类。
实体框架核心上下文类
Xamarin 应用的上下文类与 UWP 案例研究的上下文类略有不同,因为并非每个操作系统都可以在UseSQLite()
中的连接字符串中指定为不带路径的文件名。需要考虑特定于平台的差异。因此,在三个框架应用(清单 A-20 和清单 A-21 )的自定义接口IEnv
的实现中,数据库文件的路径由依赖注入提供。
Note
DAL 库需要 NuGet 包Xamarin.Forms
,因为依赖注入使用 Xamarin 表单中内置的依赖注入框架。
using Microsoft.EntityFrameworkCore;
using Xamarin.Forms;
namespace EFC_Xamarin
{
/// <summary>
/// Entity Framework context
/// </summary>
public class EFContext : DbContext
{
static public string Path { get; set; }
public DbSet<Task> TaskSet { get; set; }
public DbSet<TaskDetail> TaskDetailSet { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
EFContext.Path = System.IO.Path.Combine(DependencyService.Get<IEnv>().GetDbFolder(), "miraclelist.db");
// set provider and database file path
optionsBuilder.UseSqlite($"Filename={ EFContext.Path}");
}
}
}
Listing A-20Implementation of the Context Class in the File EFContext.cs
namespace EFC_Xamarin
{
/// <summary>
/// Custom Interface for getting the OS specific folder for the DB file
/// </summary>
public interface IEnv
{
string GetDbFolder();
}
}
Listing A-21IEnv.cs
每个操作系统都要注入一个合适的实现(清单 A-22 ,清单 A-23 ,清单 A-24 )。
using EFC_Xamarin.UWP;
using Windows.Storage;
using Xamarin.Forms;
[assembly: Dependency(typeof(Env))]
namespace EFC_Xamarin.UWP
{
public class Env : IEnv
{
public string GetDbFolder()
{
return ApplicationData.Current.LocalFolder.Path;
}
}
}
Listing A-22Implementation of IEnv on Windows 10 UWP
using EFC_Xamarin.Android;
using System;
using Xamarin.Forms;
[assembly: Dependency(typeof(Env))]
namespace EFC_Xamarin.Android
{
public class Env : IEnv
{
public string GetDbFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
}
}
Listing A-23Implementation of IEnv on Android
using EFC_Xamarin.iOS;
using System;
using System.IO;
using Xamarin.Forms;
[assembly: Dependency(typeof(Env))]
namespace EFC_Xamarin.iOS
{
public class Env : IEnv
{
public string GetDbFolder()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"..", "Library");
}
}
}
Listing A-24Implementation of IEnv on iOS
起动电码
当应用启动时,如果数据库文件尚不存在,则通过方法Database.EnsureCreated()
在App.xaml.
cs
中创建数据库文件(清单 A-25 )。
using Xamarin.Forms;
//[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace EFC_Xamarin
{
public partial class App : Application
{
public App()
{
InitializeComponent();
// Create Database if it does not exist
using (var db = new EFContext())
{
db.Database.EnsureCreated();
}
MainPage = new EFC_Xamarin.MainPage();
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
}
}
Listing A-25Extract from the App.xaml.cs File in the Project UI
生成的数据库
为 Xamarin 应用生成的数据库对应于 UWP 案例研究中的数据库。
数据访问代码
在这个简单的案例研究中,数据访问代码没有从表示层中分离出来。它还故意不使用模式 Model-View-ViewModel (MVVM ),以保持程序代码便于在本书中打印。
数据访问使用实体框架核心的异步方法来保持 UI 的响应性。
当创建子任务和删除所有任务时,会显示两个变量(列表 A-26 )。被注释掉的变量是效率较低的变量。因此,在不使用 SQL 的情况下删除所有任务和子任务需要不必要地加载所有任务并为每个任务发送一个DELETE
命令。然而,在这两种情况下都没有必要明确删除子任务,因为级联删除在标准系统中是有效的。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xamarin.Forms;
namespace EFC_Xamarin
{
public partial class MainPage : ContentPage
{
private ObservableCollection<Task> _Tasks { get; set; }
public ObservableCollection<Task> Tasks
{
get { return _Tasks; }
set { _Tasks = value; this.OnPropertyChanged(nameof(Tasks)); }
}
private string _Statustext { get; set; }
public string Statustext
{
get { return _Statustext; }
set { _Statustext = value; this.OnPropertyChanged(nameof(Statustext)); }
}
public MainPage()
{
this.BindingContext = this;
InitializeComponent();
var count = this.LoadTaskSet();
SetStatus(count + " Datensätze geladen!");
}
private async System.Threading.Tasks.Task<int> LoadTaskSet()
{
using (var db = new EFContext())
{
var list = await db.TaskSet.OrderBy(x => x.Date).ToListAsync();
Tasks = new ObservableCollection<Task>(list);
return Tasks.Count;
}
}
private void SetStatus(string text)
{
string dbstatus;
using (var db = new EFContext())
{
dbstatus = db.TaskSet.Count() + " Tasks with " + db.TaskDetailSet.Count() + " Task Details";
}
Statustext = text + " / Database Status: " + dbstatus + ")";
}
private async void Add(object sender, EventArgs e)
{
if (String.IsNullOrEmpty(C_Task.Text)) return;
// Create new Task
var t = new Task { Title = C_Task.Text, Date = C_Date.Date };
var d1 = new TaskDetail() { Text = "Plan" };
var d2 = new TaskDetail() { Text = "Execute" };
var d3 = new TaskDetail() { Text = "Run Retrospective" };
// Alternative 1
//t.Details.Add(d1);
//t.Details.Add(d2);
//t.Details.Add(d3);
// Alternative 2
t.Details.AddRange(new List<TaskDetail>() { d1, d2, d3 });
using (var db = new EFContext())
{
db.TaskSet.Add(t);
// Save now!
var count = db.SaveChangesAsync();
SetStatus(count + " records saved!");
await this.LoadTaskSet();
}
this.C_Task.Text = "";
this.C_Task.Focus();
}
private async void SetDone(object sender, EventArgs e)
{
// Get TaskID
var id = (int)((sender as Button).CommandParameter);
// Remove record
using (var db = new EFContext())
{
Task t = db.TaskSet.Include(x => x.Details).SingleOrDefault(x => x.TaskID == id);
if (t == null) return; // not found!
db.Remove(t);
int count = await db.SaveChangesAsync();
SetStatus(count + " records deleted!");
await this.LoadTaskSet();
}
}
private async void ShowDetails(object sender, EventArgs e)
{
// Get TaskID
var id = (int)((sender as Button).CommandParameter);
// Get Details
using (var db = new EFContext())
{
string s = "";
Task t = db.TaskSet.Include(x => x.Details).SingleOrDefault(x => x.TaskID == id);
s += "Task: " + t.Title + "\n\n";
s += "Due: " + String.Format("{0:dd.MM.yyyy}",t.Date) + "\n\n";
foreach (var d in t.Details)
{
s += "- " + d.Text + "\n";
}
SetStatus("Details for Task #" + id);
await this.DisplayAlert("Details for Task #" + id, s, "OK");
}
}
private async void RemoveAll(object sender, EventArgs e)
{
// Remove all tasks
using (var db = new EFContext())
{
// Alternative 1: unefficient :-(
//foreach (var b in db.TaskSet.ToList())
//{
// db.Remove(b);
//}
//db.SaveChanges();
// Alternative 2: efficient!
//db.Database.ExecuteSqlCommand("Delete from TaskDetailSet");
var count = await db.Database.ExecuteSqlCommandAsync("Delete from TaskSet");
SetStatus(count + " records deleted!");
Tasks = null;
}
}
}
}
Listing A-26Data Access Code in the MainPage.xaml.cs File
表 A-1 显示了 UWP 应用和厦门应用用户界面控件的主要区别。
餐桌 A-1
UWP-XAML vs. Xamarin-Forms XAML
| 应用 | Xamarin 形式 | | :-- | :-- | | `private async void Page_Loaded(object sender, RoutedEventArgs e)` | `protected async override void OnAppearing()` | | `this.DataContext = this;` | `this.BindingContext = this;` | | `await new MessageDialog(s, "Details for Task #" + id).ShowAsync();` | `await this.DisplayAlert("Details for Task #" + id,s,"OK");` | | `this.C_Task.Focus(FocusState.Pointer);` | `this.C_Task.Focus();` |用户界面
清单 A-27 展示了 Xamarin 应用的基于 Xamarin 表单的 UI。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Name="MainPage" xmlns:="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:EFC_Xamarin"
x:Class="EFC_Xamarin.MainPage" WidthRequest="800" HeightRequest="500" >
<Grid Margin="0,0,0,0" BackgroundColor="CornflowerBlue">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<StackLayout Margin="10,10,10,10" Grid.Row="0">
<!-- ==================== logo -->
<Image x:Name="Logo" Source="miraclelistlogo.jpg" HeightRequest="100" WidthRequest="200" HorizontalOptions="End" ></Image>
<!-- ==================== new Task -->
<Label Text="What do you have to do?" FontSize="20"></Label>
<StackLayout Orientation="Horizontal">
<ContentView BackgroundColor="White"> <DatePicker x:Name="C_Date" /></ContentView>
<Entry BackgroundColor="White" x:Name="C_Task" HorizontalOptions="FillAndExpand" Completed="Add"></Entry>
</StackLayout>
<!-- ==================== actions -->
<StackLayout Orientation="Horizontal">
<Button Clicked="Add" BackgroundColor="White" Text="Add"></Button>
<Button Clicked="RemoveAll" BackgroundColor="White" Text="Remove all" Margin="5,0,0,0"></Button>
</StackLayout>
<Label Text="Your task list:" FontSize="20"/>
<!-- ==================== list of tasks -->
<ListView x:Name="C_Tasks" ItemsSource="{Binding Tasks}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" >
<Button BackgroundColor="White" Text="Done" x:Name="C_Done" Clicked="SetDone" Margin="0,0,5,0" CommandParameter="{Binding TaskID}" />
<Button BackgroundColor="White" CommandParameter="{Binding TaskID}" FontAttributes="Bold" Text="{Binding View}" x:Name="C_Details" Clicked="ShowDetails" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
<!-- ==================== statusbar -->
<StackLayout BackgroundColor="White" Grid.Row="1">
<Label Margin="10,0,0,0" x:Name="C_StatusBar" FontSize="11" Text="{Binding StatusText}" />
</StackLayout>
</Grid>
</ContentPage>
Listing A-27MainPage.xaml
表 A-2 显示了 XAML UWP 的应用和 XAML xa marin Forms 的应用之间的主要差异。您可以看到,有许多差异使得迁移成本很高。
表 A-2
UWP XAML vs. Xamarin Forms XAML
| UWP XAML 文件 | 沙玛琳形成了 XAML | | :-- | :-- | | `