实体框架核心现代数据访问教程(一)
一、实体框架核心简介
在本章中,您将了解实体框架核心,以及它如何成为的或映射器。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对象,并将其传递给 DbContext 的基本构造函数。
看到一个例子
清单 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().ToTable( "TableName");`或`modelBuilder.Entity().ToTable ("TableName", schema: "SchemaName");` | | 列名 | 在一个属性前面:`[Column("Column Name")]` | `modelBuilder.Entity().Property(b => b.Property).HasColumnName("Column Name");` |表中列的顺序
实体框架核心按如下方式对表中的列进行排序:
- 首先,主键列按字母顺序出现。
- 然后所有其他列按字母顺序出现。
- 稍后添加的列不会按顺序排序,而是添加在后面。
与经典的实体框架不同,实体框架核心不遵循源代码中属性的顺序。微软在 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