第12章 使用 Entity Framework Core 保存数据(ASP.NET Core in Action, 2nd Edition)

发布时间 2023-04-11 13:51:24作者: 码农小白修炼记

本章包括(请点击这里阅读其他章节)

  • 什么是实体框架核心以及为什么应该使用它
  • 向 ASP.NET Core 应用程序添加实体框架核心
  • 构建数据模型并使用它创建数据库
  • 使用实体框架核心查询、创建和更新数据

使用 ASP.NET Core 构建的大多数应用程序都需要存储和加载某种数据。甚至本书中迄今为止的示例都假设您有某种存储汇率、用户购物车或实体店位置的数据存储。我虽然没有特别说明,但通常您会将这些数据存储在数据库中。

使用数据库通常是一个相当麻烦的过程。您必须管理与数据库的连接,将数据从应用程序转换为数据库能够理解的格式,并处理大量其他微妙问题。

您可以通过多种方式来管理这种复杂性,但我将重点介绍主要为 .NET Core 和 .NET 5.0 构建的库:实体框架核心(EF Core)。EF Core 是一个库,它可以让您快速轻松地为 ASP.NET Core 应用程序构建数据库访问代码。它是以流行的 Entity Framework 6.x 库为基础的,但它有显著的变化,这意味着它独立存在,不仅仅是一个升级。

本章的目的是快速概述 EF Core,以及如何在应用程序中使用它来快速查询和保存数据库。您将学习将应用程序连接到数据库以及管理数据库的模式,但不会深入讨论所有主题。

注:为了深入了解 EF Core,我建议使用 Jon P.Smith(Manning,2021)编写的《Entity Frameword Core in Action》(第二版)。或者,您可以在 Microsoft 文档网站上阅读 EF Core 及其近亲——实体框架(Entity Framework 简称 EF ),网址为 https://docs.microsoft.com/ef/core/

第 12.1 节介绍 EF Core,并解释了为什么您可能希望在应用程序中使用它。您将了解 EF Core 的设计如何帮助您快速映谢数据库结构并减少与数据库交互的冲突。

在第 12.2 节中,您将学习如何将 EF Core 添加到 ASP.NET Core 应用程序中,并使用 ASP.NET Core 配置系统对其进行设置。您将看到如何为应用程序建立一个用来表示存储在数据库中的数据的模型,以及如何将其连接到 ASP.NET Core DI 容器中。

注意:对于本章和本书的其余部分,我将使用 SQL Server Express 的 LocalDB 功能。这是作为 Visual Studio 2019 的一部分安装的(当您选择 ASP.NET 和 Web 开发工作时),它提供了一个轻量级的 SQL Server 引擎。例如,本书的代码示例包括使用 SQLite 的版本。

注:您可以在 Microsoft 的“SQL Server Express LocalDB”文档中阅读有关 LocalDB 的更多信息,请访问 http://mng.bz/5jEa

无论您如何精心设计原始数据模型,您都可能需要更改它。在第 12.3 节中,我将介绍如何轻松更新模型并将这些更改应用到数据库本身,从而使用 EF Core 完成所有繁重的工作。

在配置了 EF Core 并创建了数据库后,第 12.4 节将介绍如何在应用程序代码中使用 EF Core。您将了解如何创建、读取、更新和删除(CRUD)记录,并了解在设计数据访问时要使用的一些模式。

在第 12.5 节中,我强调了在编写应用程序中使用 EF Core 时需要考虑的几个问题。

本章提供所有关于 EF Core 相关概念的简要介绍,因此,如果您选择在自己的应用程序中使用 EF Core,特别是如果这是您第一次使用这种方式访问数据库,我强烈建议您在掌握本章的基础知识后再去多阅读其他更深入的内容。

在我们开始编写任何代码之前,让我们看看 EF Core 是什么,它解决了什么问题,以及您可能希望何时使用它。

12.1 引入实体框架核心(Entity Framwork Core 简称 EF Core)

数据库访问代码在 Web 应用程序中无处不在。无论您正在构建电子商务应用程序、博客还是 Next Big Thing™, 你可能都需要与数据库交互。

不幸的是,从应用程序代码与数据库交互通常是一件麻烦事,您可以采取许多不同的方法。例如,从数据库读取数据这样简单的事情需要处理网络连接、编写 SQL 语句和处理数据。.NET 生态系统有一系列可用的库,从低级 ADO.NET 库到高级抽象(如 EF Core)。

在本节中,我将描述 EF Core 是什么以及使用它可以解决的问题。我介绍了使用 EF Core 抽象背后的动机,以及它如何帮助弥合应用程序代码和数据库之间的鸿沟。首先,我分析了一些在应用程序中使用它的利弊,这将有助于您决定它是否适合您的目的。然后,我们将查看一个示例 EF Core 完成从应用程序代码到数据库映射,以了解 EF Core 的主要概念。

12.1.1 什么是 EF Core?

EF Core 是一个库,它提供了一种面向对象的方式来访问数据库。它充当对象关系映射器(ORM),为您与数据库通信,并将数据库响应映射到 .NET 类和对象,如图12.1所示。

定义:使用对象关系映射器(ORM),可以通过将类和对象等面向对象的概念映射到表和列等数据库概念来操作数据库。

EF Core 基于现有的实体框架库(当前版本为 6.x),但与之不同。它是作为 .NET Core 跨平台推送工作的一部分构建的,但考虑到了其他目标。特别是,EF Core 团队希望制作一个可用于多种数据库高性能的库。

有许多不同类型的数据库,但最常用的家族可能是关系数据库,使用结构化查询语言(SQL)进行访问。这是 EF Core 的存在的意义;它可以映射 Microsoft SQL Server、MySQL、Postgres 和许多其他关系数据库。它甚至有一个很酷的特性,您可以在测试时使用它来创建一个临时数据库。EF Core 提供程序模型,可以在必要时提供对其他关系数据库的支持。

图12.1 EF Core 将.NET类和对象映射到表和行等数据库概念。

注:从 .NET Core 3.0 开始,EF Core 现在也可以使用非关系、NoSQL 或文档数据库,如 Cosmos DB。然而,在本书中,我只考虑映射到关系数据库,因为这是我经验中最常见的需求。从历史上看,大多数数据访问,尤其是在 .NET 生态系统中,一直使用关系数据库,因此它通常仍然是最流行的方法。

这涵盖了 EF Core 是什么,但它没有深入了解为什么要使用它。为什么不使用传统的 ADO.NET 库直接访问数据库?使用 EF Core 的大多数论点都可以应用于 ORM,那么 ORM 的优点是什么?

12.1.2 为什么使用对象关系映射器?

ORM 带来的最大优势之一是开发应用程序的速度。您可以停留在熟悉的面向对象.NET领域,通常不需要直接操作数据库或编写自定义 SQL。

假设您有一个电子商务站点,您希望从数据库中加载产品的详细信息。使用低级数据库访问代码,您必须打开与数据库的连接,使用正确的表和列名编写必要的 SQL,通过连接读取数据,创建 POCO 以保存数据,并手动设置对象的属性,将数据转换为正确的格式。听起来很痛苦,对吧?

ORM(如 EF Core)为您处理大部分事务。它处理与数据库的连接,生成 SQL,并将数据映射回 POCO 对象。您只需要提供一个 LINQ 查询来描述您想要检索的数据。

ORM 作为数据库的高级抽象,因此可以显著减少与数据库交互所需的管道代码量。在最基本的层次上,它们负责将 SQL 语句映射到对象,反之亦然,但大多数 ORM 更进一步,提供了额外的功能。

像 EF Core 这样的 ORM 会跟踪从数据库中检索到的任何对象的哪些属性发生了更改。这允许您通过从数据库表映射对象,在 .NET 代码中修改对象,然后请求 ORM 更新数据库中的关联记录,从而从数据库中加载对象。ORM 将确定哪些适当的关系已更改,并为相应的列发布更新语句,从而为您节省大量工作。

正如软件开发中经常出现的情况一样,使用 ORM 有其缺点。ORM 最大的优点之一也是它们的致命弱点——它们向您隐藏数据库。有时,这种高级别的抽象会导致应用程序中出现问题的数据库查询模式。一个经典的例子是 N+1 问题,在这个问题中,应该是单个数据库请求的内容会变成对数据库表中每一行的单独请求。

另一个常见的缺点是性能。ORM 是对多个概念的抽象,因此它们本质上比你手工制作应用程序中的每一条数据访问要做更多的工作。包括 EF Core 在内的大多数 ORM 都在一定程度上权衡性能,以便于开发。

也就是说,如果您意识到 ORM 的缺陷,通常可以大大简化与数据库交互所需的代码。与任何事情一样,如果抽象对你有用,就使用它;否则,不要。如果您只有最低的数据库访问要求,或者您需要获得最佳性能,那么像 EF Core 这样的 ORM 可能不适合。

另一种选择是两全其美:使用 ORM 快速开发大部分应用程序,然后返回到较低级别的 API(如 ADO.NET)来处理那些被证明是应用程序瓶颈的少数领域。这样,您可以使用 EF Core 获得足够好的性能,以性能换取开发时间,并只优化那些需要它的领域。

即使您决定在应用程序中使用 ORM,也有许多不同的 ORM 可用于 .NET,EF Core 就是其中之一。EF Core 是否适合您将取决于您需要的功能以及您愿意做出的权衡。下一节将 EF Core 与微软的其他产品实体框架进行比较,但您可以考虑其他许多替代方案,如 Dapper 和 NHibernate,每个都有自己的权衡。

12.1.3 您应该何时选择 EF Core?

微软将 EF Core 设计为对 2008 年发布的成熟实体框架 6.x(EF 6.x)ORM 的重新构想。经过十年的开发,EF6.x 是一个稳定且功能丰富的 ORM。

相比之下,EF Core 是一个相对较新的项目。EF Core 的 API 被设计为与 EF 6.x 的 API 接近,尽管它们并不相同,但核心组件已经被完全重写。您应该认为 EF Core 与 EF 6.x 不同;从 EF6.x 直接升级到 EF Core 是非常重要的。

Microsoft 同时支持 EF Core 和 EF 6.x,而且两者都会不断改进,因此您应该选择哪一种?你需要考虑很多事情:

  • 跨平台 —— EF Core 5.0 以 .NET 标准为目标,因此可以在以 .NET Core 3.0 或更高版本为目标的跨平台应用中使用。从 6.3 版开始,EF6.x 也是跨平台的,在 .NET5.0 上运行时存在一些限制,例如不支持设计器。
  • 数据库提供程序 —— EF6.x 和 EFCore 都允许您使用可插入的提供程序连接到各种数据库类型。EF Core 有越来越多的提供程序,但 EF 6.x 的提供程序没有这么多,特别是如果您想在 .NET 5.0 上运行 EF 6.x。如果您正在使用的数据库没有提供商,这有点破坏体验!
  • 性能 —— EF 6.x 的性能在其记录上有点黑,所以 EF Core 旨在纠正这一点。EF Core 的设计是快速和轻量级的,显著优于 EF 6.x。但是它不太可能达到更轻量级的 ORM(如 Dapper 或手工 SQL 语句)的性能。
  • 功能 —— 功能是 EF 6.x 和 EF Core 之间最大的差异,尽管 EF Core 5.0 的差异比以往任何时候都小。EF Core 现在有许多 EF 6.x 没有的功能(批处理状态、客户端密钥生成、内存数据库测试)。与 EF 6.x 相比,EF Core 仍然缺少一些功能,例如存储过程映射和表每具体类型(TPC),但由于 EF Core 正在积极开发中,这些功能正在等待实现。相比之下,EF 6.x 可能只看到增量改进和 bug 修复,而不是主要的功能添加。

这些权衡和限制对你来说是否是一个问题,将在很大程度上取决于你的特定应用。考虑到这些限制,启用一个新的要比以后尝试解决这些限制容易得多。

提示:不建议每个人都使用 EF Core,但对于新应用程序建议使用 EF Core。确保您了解所涉及的权衡,并关注EF团队的指导:https://docs.microsoft.com/ef/efcore-and-ef6

如果您正在开发一个新的 ASP.NET Core 应用程序,您希望使用 ORM 进行快速开发,并且不需要其他在 EF Core 中不可用的功能,那么 EF Core 是一个很好的选择。ASP.NET Core 的其他子系统也支持它。例如,在第14章中,您将看到如何将 EF Core 与 ASP.NET Core 身份验证系统一起用于管理应用程序中的用户。

在我们深入了解在应用程序中使用 EF Core 的实质之前,我将描述我们将使用的应用程序,作为本章的案例研究。我们将介绍应用程序和数据库的详细信息,以及如何使用 EF Core 在两者之间进行通信。

12.1.4 将数据库映射到应用程序代码

EF Core 专注于应用程序和数据库之间的通信,所以为了展示它,我们需要一个应用程序。本章使用了一个简单的烹饪应用程序的示例,它列出了食谱,并允许您查看食谱的成分,如图 12.2 所示。用户可以浏览配方、添加新配方、编辑配方和删除旧配方。

这显然是一个简单的应用程序,但它包含了您与其两个实体(Recipe 和 Ingredient)所需的所有数据库交互。

图12.2 烹饪应用程序列出了食谱。您可以查看、更新和删除配方,或创建新配方。

定义:实体是由 EF Core 映射到数据库的 .NET 类。这些是您定义的类,通常是 POCO 类,可以通过使用 EF Core 映射到数据库表来保存和加载。

当您与 EF Core 交互时,您将主要使用 POCO 实体和继承自 DbContext EFCore 类的数据库上下文。实体类是数据库中表的面向对象表示;它们表示要存储在数据库中的数据。您在应用程序中使用 DbContext 来配置 EF Core 并在运行时访问数据库。

注意:应用程序中可能有多个 DbContext,甚至可以将它们配置为与不同的数据库集成。

当应用程序首次使用 EFCore 时,EFCore 基于应用程序的 DbContext 上的 DbSet<T>属性和实体类本身创建数据库的内部表示,如图 12.3 所示。

图12.3 EF Core通过探索代码中的类型来创建应用程序数据模型的内部模型。它将添加应用程序的DbContext上DbSet<>属性中引用的所有类型以及任何链接类型。

对于您的配方应用程序,EF Core将构建配方类的模型,因为它在AppDbContext上作为DbSet<recipe>公开。此外,EF Core将遍历Recipe上的所有属性,查找它不知道的类型,并将它们添加到其内部模型中。在您的应用程序中,Recipe上的Ingredients集合将Ingredient实体显示为ICollection<Ingredient>,因此EF Core对实体进行了适当建模。

每个实体都映射到数据库中的一个表,但EFCore也映射实体之间的关系。每个配方可以有许多成分,但每个成分(有名称、数量和单位)都属于一个配方,因此这是一种多对一的关系。EF Core使用这些知识来正确地建模等效的多对一数据库结构。

注意:两种不同的食谱,比如鱼馅饼和柠檬鸡,可能使用相同名称和数量的配料,例如一个柠檬的汁,但它们基本上是两个不同的例子。如果您将柠檬鸡配方更新为使用两个柠檬,那么您不会希望此更改自动更新鱼馅饼以使用两个!

EF Core在与数据库交互时使用其构建的内部模型。这确保它构建正确的SQL来创建、读取、更新和删除实体。

是的,是时候编写一些代码了!在下一节中,您将开始构建配方应用程序。您将看到如何将EFCore添加到ASP.NET Core应用程序、配置数据库提供程序以及设计应用程序的数据模型。

12.2向应用程序添加EF Core

在本节中,我们将重点介绍如何在ASP.NET Core配方应用程序中安装和配置EF Core。您将学习如何安装所需的NuGet软件包以及如何为应用程序构建数据模型。当我们在本章中讨论EF Core时,我不打算讨论如何创建应用程序——我创建了一个简单的Razor Pages应用程序作为基础,没有什么花哨的。

提示:本章的示例代码显示了本章中三个点的应用程序状态:第12.2节末尾、第12.3节末尾和本章末尾。它还包括使用LocalDB和SQLite提供程序的示例。

示例应用程序中与EFCore的交互发生在服务层中,该服务层封装RazorPages框架之外的所有数据访问,如图12.4所示。这将使您的关注点分离,并使您的服务可测试。

图12.4通过使用EF Core从数据库加载数据来处理请求。与EF Core的交互仅限于RecipeService——Razor Page不直接访问EF Core。

将EF Core添加到应用程序是一个多步骤过程:

  1. 选择数据库提供程序;例如,Postgres、SQLite或MS SQL Server。
  2. 安装EF Core NuGet软件包。
  3. 设计应用程序的DbContext和构成数据模型的实体。
  4. 在ASP.NET Core DI容器中注册应用程序的DbContext。
  5. 使用EF Core生成描述数据模型的迁移。
  6. 将迁移应用于数据库以更新数据库的架构。

这可能看起来有点令人望而生畏,但我们将在本节中完成步骤1-4,并在第12.3节中完成第5-6步,因此不会花费很长时间。考虑到本章的空间限制,我将在展示的代码中坚持EF Core的默认约定。EF Core的可定制性远高于最初的外观,但我鼓励您尽可能坚持默认设置。从长远来看,这会让你的生活更轻松。

设置EFCore的第一步是决定要与哪个数据库交互。客户或公司的政策很可能会对你做出决定,但还是值得考虑一下。

12.2.1 选择数据库提供程序并安装EF Core

EF Core通过使用提供者模型支持一系列数据库。EF Core的模块化特性意味着您可以使用相同的高级API对不同的底层数据库进行编程,并且EF Core知道如何生成必要的特定于实现的代码和SQL语句。

当您启动应用程序时,您可能已经有了一个数据库,您会很高兴地知道EF Core已经涵盖了大多数流行的数据库。

添加对给定数据库的支持涉及将正确的NuGet包添加到.csproj文件中。例如

  • PostgreSQL—Npgsql.EntityFrameworkCore.PostgreSQL
  • Microsoft SQL Server —Microsoft.EntityFrameworkCore.SqlServer
  • MySQL—MySql.Data.EntityFrameworkCore
  • SQLite—Microsoft.EntityFrameworkCore.SQLite

有些数据库提供程序包由Microsoft维护,有些由开源社区维护,有些可能需要付费许可证(例如,Oracle提供程序),因此请务必检查您的要求。您可以在以下位置找到提供商列表https://docs.microsoft.com/ef/core/providers/.

您可以使用与任何其他库相同的方式将数据库提供程序安装到应用程序中:将NuGet包添加到项目的.csproj文件中,并从命令行运行dotnet还原(或让Visual Studio自动为您还原)。EF Core本质上是模块化的,因此您需要安装多个软件包。我正在为配方应用程序使用带有LocalDB的SQL Server数据库提供程序,因此我将使用SQL Server软件包:

  • Microsoft.EntityFrameworkCore.SqlServer——这是在运行时使用EF Core的主要数据库提供程序包。它还包含对主要EF Core NuGet包的引用。
  • Microsoft.EntityFrameworkCore.Design——它包含EF Core的共享设计时组件。

提示:您还需要安装工具来帮助您创建和更新数据库。我在第12.3.1节中介绍了如何安装这些。

清单12.1显示了添加EFCore包后配方应用程序的.csproj文件。记住,将NuGet包添加为PackageReference元素。

"Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> //该应用程序以.NET 5.0为目标。 </PropertyGroup> <ItemGroup> //为所选数据库安装相应的NuGet包。 <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" /> //包含EF Core的共享设计时组件 <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0" /> </ItemGroup> </Project>

安装并恢复这些包后,您就拥有了开始为应用程序构建数据模型所需的一切。在下一节中,我们将为配方应用程序创建实体类和DbContext。

12.2.2 建立数据模型

在第12.1.4节中,我概述了EF Core如何从DbContext和实体模型构建数据库的内部模型。除了这种发现机制之外,EFCore还非常灵活,可以让您以自己想要的方式将实体定义为POCO类。

有些ORM要求实体从特定基类继承,或者用属性修饰模型以描述如何映射它们。EF Core非常倾向于约定而非配置方法,如您在本列表中所见,该列表显示了应用程序的Recipe和Ingredient实体类。

set; } public string Name { get; set; } public TimeSpan TimeToCook { get; set; } public bool IsDeleted { get; set; } public string Method { get; set; } public ICollection<Ingredient> Ingredients { get; set; } //一个配方可以有许多成分,由ICollection表示。 } public class Ingredient { public int IngredientId { get; set; } public int RecipeId { get; set; } public string Name { get; set; } public decimal Quantity { get; set; } public string Unit { get; set; } }

这些类符合某些默认约定,EFCore使用这些约定来构建其映射的数据库的图片。例如,Recipe类具有RecipeId属性,Ingredient类具有IngredientId属性。EF Core将Id后缀的这种模式标识为表的主键。

定义:表的主键是唯一标识表中所有其他行的值。它通常是int或Guid。

此处可见的另一个约定是Ingredient类上的RecipeId属性。EF Core将其解释为指向Recipe类的外键。当在Recipe类上使用ICollection<Ingredient>时,这表示一种多对一的关系,其中每个配方都有很多成分,但每个成分都只属于一个配方,如图12.5所示。

图12.5 代码中的多对一关系被转换为表之间的外键关系。

定义:表上的外键指向不同表的主键,形成两行之间的链接。

这里还有许多其他约定,例如EF Core将为数据库表和列使用的名称,或者它将为每个属性使用的数据库列类型,但我不打算在这里讨论它们。EF Core文档包含有关所有约定的详细信息,以及如何为您的应用程序定制这些约定:https://docs.microsoft.com/ef/core/modeling/.

提示:您还可以使用DataAnnotations属性来修饰实体类,控制列命名或字符串长度等内容。EF Core将使用这些属性覆盖默认约定。

除了实体之外,还可以为应用程序定义DbContext。这是应用程序中EF Core的核心,用于所有数据库调用。创建一个自定义DbContext,在本例中称为AppDbContext,并从DbContext基类派生,如下所示。这会暴露DbSet<Recipe>,因此EF Core可以发现并映射Recipe实体。对于应用程序中的每个顶级实体,可以以这种方式公开DbSet<>的多个实例。

base(options) { } public DbSet<Recipe> Recipes { get; set; } //您将使用Recipes属性来查询数据库。 }

应用程序的AppDbContext很简单,包含根实体的列表,但在更复杂的应用程序中,可以使用它做更多的工作。如果需要,您可以完全自定义EF Core如何将实体映射到数据库,但对于这个应用程序,您将使用默认值。

注意:您没有在AppDbContext中列出成分,但它将由EF Core建模,因为它在配方中公开。您仍然可以访问数据库中的Ingredient对象,但必须通过Recipe实体的Ingredients属性进行导航,如第12.4节所示。

对于这个简单的示例,数据模型由以下三个类组成:AppDbContext、Recipe和Ingredient。这两个实体将映射到表,它们的列将映射到属性,您将使用AppDbContext来访问它们。

注意:这种代码优先的方法是典型的,但是如果您有一个现有的数据库,您可以自动生成EF实体和DbContext。(更多信息可在EF Core文档中的Microsoft“逆向工程”文章中找到:http://mng.bz/mgd4.)

数据模型已经完成,但您还没有完全准备好使用它。ASP.NET Core应用程序不知道如何创建AppDbContext,AppDbContext需要一个连接字符串,以便它可以与数据库通信。在下一节中,我们将解决这两个问题,并在ASP.NET Core应用程序中完成EF Core的设置。

12.2.3注册数据上下文

与ASP.NET Core中的任何其他服务一样,您应该向DI容器注册AppDbContext。在注册上下文时,还需要配置数据库提供程序并设置连接字符串,因此EFCore知道如何与数据库通信。

您可以在Startup.cs的ConfigureServices方法中注册AppDbContext。EF Core为此提供了一个通用的AddDbContext<T>扩展方法,该方法为DbContextOptionsBuilder实例提供配置函数。该生成器可用于设置EF Core的一系列内部属性,并允许您根据需要完全替换EF Core的内部服务。

你的应用程序的配置同样很好,也很简单,你可以在下面的列表中看到。使用UseSqlServer扩展方法设置数据库提供程序,该方法由Microsoft.EntityFrameworkCore.SqlServer包提供,并向其传递连接字符串。

清单12.4 向DI容器注册DbContext

//连接字符串取自ConnectionStrings部分的配置。
public void ConfigureServices(IServiceCollection services)
{
    var connString = Configuration.GetConnectionString("DefaultConnection");

    services.AddDbContext<AppDbContext>(  //通过将应用程序的DbContext用作泛型参数来注册它。
        options => options.UseSqlServer(connString));  ////在DbContext的自定义选项中指定数据库提供程序。

    // 添加其他服务。
}

注意:如果您使用不同的数据库提供程序,例如SQLite提供程序,则在注册AppDbContext时,需要在options对象上调用适当的Use*方法。

正如我在上一章中所讨论的,连接字符串是一个典型的秘密,因此从配置中加载它是有意义的。在运行时,将使用当前环境的正确配置字符串,因此在本地和生产环境中开发时可以使用不同的数据库。

提示:您可以以其他方式配置AppDbContext并提供连接字符串,例如使用OnConfiguring方法,但我建议ASP.NET Core网站使用此处显示的方法。

现在,您有一个注册到DI容器的DbContext、AppDbContext和一个对应于数据库的数据模型。代码方面,您已经准备好开始使用EF Core,但您没有的是数据库!在下一节中,您将了解如何轻松使用.NET CLI来确保数据库与EF Core数据模型保持最新。

12.3 通过迁移管理更改

在本节中,您将学习如何使用迁移生成SQL语句,以使数据库的模式与应用程序的数据模型保持同步。您将学习如何创建初始迁移并使用它创建数据库。然后,您将更新数据模型,创建第二个迁移,并使用它更新数据库模式。

管理数据库的模式更改(如需要添加新表或新列时)是众所周知的困难。您的应用程序代码显式地绑定到数据库的特定版本,您需要确保两者始终同步。

定义:模式是指数据库中数据的组织方式,包括表、列以及它们之间的关系。

当您部署应用程序时,通常可以删除旧代码/可执行文件,并用新代码替换它——作业已完成。如果需要回滚更改,请删除新代码并部署应用程序的旧版本。

数据库的困难在于它们包含数据!这意味着不可能在每次部署时都将其销毁并创建一个新的数据库。

一种常见的最佳实践是将数据库的模式与应用程序的代码一起显式地版本化。您可以通过多种方式实现这一点,但通常需要存储数据库的前一个模式和新模式之间的差异,通常作为SQL脚本。然后,您可以使用DbUp和FluentMigrator3等库来跟踪已应用的脚本,并确保数据库架构是最新的。或者,您可以使用外部工具为您管理。

EF Core提供了自己的模式管理版本,称为迁移。迁移提供了一种在EF Core数据模型更改时管理数据库模式更改的方法。迁移是应用程序中的一个C#代码文件,它定义了数据模型是如何更改的——添加了哪些列、新实体等等。迁移提供了数据库架构作为应用程序的一部分如何随时间演变的记录,因此架构始终与应用程序的数据模型同步。

您可以使用命令行工具从迁移中创建新数据库,或通过对现有数据库应用新迁移来更新现有数据库。您甚至可以回滚迁移,从而将数据库更新为以前的架构。

警告:应用迁移会修改数据库,因此您必须始终注意数据丢失。如果使用迁移从数据库中删除表,然后回滚迁移,则将重新创建该表,但它以前包含的数据将永远消失!

在本节中,您将了解如何创建第一个迁移并使用它创建数据库。然后,您将更新数据模型,创建第二个迁移,并使用它更新数据库模式。

12.3.1 创建第一次迁移

在创建迁移之前,需要安装必要的工具。有两种主要方法可以做到这一点:

  • 包管理器控制台——可以在Visual Studio的包管理器console(PMC)中使用PowerShell cmdlet。您可以直接从PMC安装它们,也可以将Microsoft.EntityFrameworkCore.Tools包添加到项目中。
  • .NET工具——可以从命令行运行的跨平台工具,它扩展了.NET SDK。通过运行dotnet工具install --global dotnet-ef,可以为您的计算机全局安装这些工具。

在本书中,我将使用跨平台的.NET工具,但如果您熟悉EF6.x或更喜欢使用VisualStudioPMC,那么您将要执行的所有步骤都有等效的命令。5您可以通过运行dotnet-EF来检查.NET工具是否正确安装。这将产生一个如图12.6所示的帮助屏幕。

图12.6 运行dotnet-ef命令以检查.NET ef Core工具是否正确安装

提示:如果在运行上述命令时收到“No executable found matching command'dotnet-ef'”消息,请确保已使用dotnet-tool install--global dotnet-ef安装了全局工具。通常,您需要从已注册AppDbContext的项目文件夹中运行dotnet-ef工具(而不是在解决方案文件夹级别)。

安装了工具并配置了数据库上下文后,可以通过在web项目文件夹中运行以下命令并为迁移提供名称(在本例中为InitialSchema)来创建第一次迁移:

dotnet ef migrations add InitialSchema

此命令在项目的Migrations文件夹中创建三个文件:

  • 迁移文件——Timestamp_MigrationName.cs格式的文件。这描述了要对数据库执行的操作,例如创建表或添加列。请注意,此处生成的命令是特定于数据库提供程序的,基于项目中配置的数据库提供程序。
  • Migrationdesigner.cs文件——该文件描述了生成迁移时数据模型的EFCore内部模型。
  • AppDbContextModelSnapshot.cs——This描述了EF Core当前的内部模型。当您添加另一个迁移时,将更新此迁移,因此它应始终与当前最新的迁移相同。
    EF Core可以在创建新迁移时使用AppDbContextModelSnapshot.cs来确定数据库的先前状态,而无需直接与数据库交互。

这三个文件封装了迁移过程,但添加迁移不会更新数据库本身中的任何内容。为此,必须运行其他命令才能将迁移应用于数据库。

提示:在运行以下命令之前,您可以也应该查看EFCore生成的迁移文件,以检查它将对数据库执行什么操作。安全总比抱歉好!

您可以通过以下三种方式之一应用迁移:

  • 使用.NET工具
  • 使用Visual Studio PowerShell cmdlet
  • 在代码中,通过从DI容器获取AppDbContext的实例并调用context.Database.Migrate()

哪个最适合您取决于您如何设计应用程序、如何更新生产数据库以及您的个人偏好。现在我将使用.NET工具,但我将在第12.5节中讨论其中一些注意事项。

您可以通过运行

dotnet ef database update

从应用程序的项目文件夹中。我将不详细介绍它是如何工作的,但此命令执行四个步骤:

  1. 构建应用程序。
  2. 加载应用程序的Startup类中配置的服务,包括AppDbContext。
  3. 检查AppDbContext连接字符串中的数据库是否存在。如果没有,它会创建它。
  4. 通过应用任何未应用的迁移来更新数据库。

如果一切都配置正确,如我在第12.2节中所示,那么运行此命令将为您设置一个闪亮的新数据库,如图12.7所示。

注意:如果在运行这些命令时收到错误消息“找不到项目”,请检查是否在应用程序的项目文件夹中运行这些命令,而不是在顶级解决方案文件夹中。

图12.7 将迁移应用到数据库将创建数据库(如果数据库不存在),并更新数据库以匹配EF Core的内部数据模型。应用的迁移列表存储在EFMigrationHistory表中。

当您将迁移应用到数据库时,EFCore会在数据库中创建必要的表,并添加适当的列和键。您可能还注意到EFMigrationsHistory表。EF Core使用它来存储应用于数据库的迁移名称。下次运行dotnet-ef数据库更新时,ef Core可以将此表与应用程序中的迁移列表进行比较,并将仅将新的迁移应用于数据库。

在下一节中,我们将讨论如何轻松地更改数据模型和更新数据库模式,而无需从头创建数据库。

12.3.2 添加第二次迁移

无论是由于范围的扩大还是简单的维护,大多数应用程序都不可避免地会发生变化。向实体添加属性、完全添加新实体以及删除过时的类——所有这些都是可能的。

EF Core迁移使这变得简单。假设你决定在你的食谱应用程序中突出素食和纯素菜肴,在食谱实体上显示IsVegetarian和IsVegan属性。将实体更改为所需状态,生成迁移,并将其应用于数据库,如图12.8所示。

图12.8 创建第二个迁移并使用命令行工具将其应用于数据库。

set; } public string Name { get; set; } public TimeSpan TimeToCook { get; set; } public bool IsDeleted { get; set; } public string Method { get; set; } public bool IsVegetarian { get; set; } public bool IsVegan { get; set; } public ICollection<Ingredient> Ingredients { get; set; } }

更改实体后,需要更新EFCore的数据模型内部表示。通过调用dotnet-ef-migrations-add并为迁移提供名称,可以以与第一次迁移完全相同的方式执行此操作:

dotnet ef migrations add ExtraRecipeFields

这将通过添加迁移文件及其.designer.cs快照文件并更新AppDbContextModelSnapshot.cs,在项目中创建第二次迁移,如图12.9所示。

图12.9 添加第二个迁移将添加新的迁移文件和迁移Designer.cs文件。它还更新AppDbContextModelSnapshot以匹配新迁移的Designer.cs文件。

与之前一样,这会创建迁移的文件,但不会修改数据库。您可以通过运行

dotnet ef database update

这会将应用程序中的迁移与数据库中的__EFMigrationsHistory表进行比较,以查看哪些迁移未完成,然后运行它们。EF Core将运行20200511204457_ExtraRecipeFields迁移,将IsVegetarian和IsVegan字段添加到数据库中,如图12.10所示。

图12.10 将ExtraRecipeFields迁移应用于数据库将IsVegetarian和IsVegan字段添加到Recipes表中。

使用迁移是确保在源代码管理中对数据库和应用程序代码进行版本控制的一个好方法。您可以很容易地查看应用程序的源代码以查找历史时间点,并重新创建应用程序当时使用的数据库模式。

当您单独工作或部署到单个web服务器时,迁移很容易使用,但即使在这些情况下,在决定如何管理数据库时,也需要考虑一些重要事项。对于具有使用共享数据库的多个web服务器的应用程序,或者对于容器化应用程序,您需要考虑更多的问题。

这本书是关于ASP.NET Core,而不是EF Core,所以我不想过多地讨论数据库管理,但第12.5节指出了在生产中使用迁移时需要记住的一些事项。

在下一节中,我们将回到重要的内容——定义业务逻辑并对数据库执行CRUD操作。

12.4 从数据库查询数据并将数据保存到数据库

让我们回顾一下您在创建配方应用程序中的位置:

  • 您为应用程序创建了一个简单的数据模型,包括食谱和配料。
  • 您为数据模型生成了迁移,以更新EF Core的实体内部模型。
  • 您将迁移应用于数据库,因此其模式与EFCore的模型相匹配。

在本节中,您将通过创建RecipeService来构建应用程序的业务逻辑。这将处理查询数据库中的配方、创建新配方和修改现有配方。由于此应用程序只有一个简单的域,我将使用RecipeService来处理所有需求,但在您自己的应用程序中,您可能会有多个服务协作以提供业务逻辑。

注意:对于简单的应用程序,您可能会尝试将此逻辑移动到Razor Pages中。我鼓励你抵制这种冲动;将业务逻辑提取到其他服务将RazorPages和WebAPI的以HTTP为中心的特性与底层业务逻辑分离。这通常会使您的业务逻辑更易于测试和重用。

我们的数据库中还没有任何数据,所以我们最好先让您创建一个配方。

12.4.1 创建记录

在本节中,您将构建允许用户在应用程序中创建配方的功能。这将主要由一个表单组成,用户可以使用Razor Tag Helpers输入配方的所有详细信息,您在第7章和第8章中了解了这一点。此表单发布到Create.cshtmlRazorPage,该页面使用模型绑定和验证属性来确认请求是否有效,如第6章所示。

如果请求有效,页面处理程序调用RecipeService在数据库中创建新的Recipe对象。由于EF Core是本章的主题,我将单独关注这项服务,但如果您想了解所有内容是如何结合在一起的,您可以随时查看本书的源代码。

在这个应用程序中创建配方的业务逻辑很简单——没有逻辑!将Create.cshtml Razor页面中提供的命令绑定模型映射到Recipe实体及其成分,将Recipe对象添加到AppDbContext,并将其保存在数据库中,如图12.11所示。

图12.11 调用Create.cshtml Razor页面并创建新实体。配方是从CreateRecipeCommand绑定模型创建的,并添加到DbContext中。EF Core生成SQL以向数据库中的Recipes表添加新行。

警告:许多使用EF或EF Core的简单等效示例应用程序允许您直接绑定到Recipe实体,作为MVC操作的视图模型。不幸的是,这暴露了一个被称为“过度预测”的安全漏洞,这是一种糟糕的做法。如果希望避免应用程序中的样板映射代码,请考虑使用AutoMapper等库(http://automapper.org/). 欲了解更多关于过度投资的详细信息,请参阅我的博客文章:http://mng.bz/d48O.

在EF Core中创建实体涉及向映射表添加新行。对于您的应用程序,无论何时创建新配方,您都可以添加链接的成分实体。EF Core负责通过为数据库中的每种成分创建正确的RecipeId来正确链接这些成分。

本示例所需的大部分代码涉及从CreateRecipeCommand转换到Recipe实体——与AppDbContext的交互仅包含两个方法:Add()和SaveChangesAsync()。

清单12.6 在数据库中创建配方实体

readonly AppDbContext _context;  //AppDbContext的实例使用DI注入到类构造函数中。
public async Task<int> CreateRecipe(CreateRecipeCommand cmd)  //CreateRecipeCommand是从Razor Page处理程序传入的。
{
    //通过从命令对象映射到配方实体来创建配方。
    var recipe = new Recipe
    {
        Name = cmd.Name, 
        TimeToCook = new TimeSpan(cmd.TimeToCookHrs, cmd.TimeToCookMins, 0), 
        Method = cmd.Method,
        IsVegetarian = cmd.IsVegetarian, 
        IsVegan = cmd.IsVegan,
        Ingredients = cmd.Ingredients?.Select(i => 
            //将每个CreateIngredientCommand映射到Ingredient实体上。
            new Ingredient
            {
                Name = i.Name, Quantity = i.Quantity, 
                Unit = i.Unit,
            }).ToList()
    };

    _context.Add(recipe);  //告诉EF Core跟踪新实体。
    await _context.SaveChangesAsync();   //保存新配方时,EF Core会在新配方上填充RecipeId字段。
    return recipe.RecipeId;  ////告诉EF Core将实体写入数据库。这使用命令的异步版本。
}

与EFCore和数据库的所有交互都以AppDbContext的实例开始,该实例通常通过构造函数注入DI。创建新实体需要三个步骤:

  1. 创建配方和成分实体。
  2. 使用_context.Add(实体)将实体添加到EF Core的跟踪实体列表中。
  3. 对数据库执行SQL INSERT语句,通过调用_context.SaveChangesAsync()将必要的行添加到Recipe和Ingredient表中。

提示:大多数涉及与数据库交互的EF Core命令都有同步和异步版本,例如SaveChanges()和SaveChangesAsync()。一般来说,异步版本将允许您的应用程序处理更多的并发连接,所以我倾向于在任何时候使用它们。

如果EFCore尝试与数据库交互时出现问题——例如,您没有运行迁移来更新数据库模式——它将抛出异常。我没有在这里展示过,但在应用程序中处理这些问题很重要,这样在出现问题时就不会向用户显示难看的错误页面。

假设一切顺利,EF Core会更新所有自动生成的实体ID(RecipeId on Recipe,RecipeId和IngredientId on Ingredient)。将配方ID返回到Razor页面,以便其使用;例如重定向到“查看配方”页面。

就这样,您已经使用EF Core创建了第一个实体。在下一节中,我们将从数据库中加载这些实体,以便您可以在列表中查看它们。

12.4.2 加载记录列表

现在您可以创建食谱了,您需要编写代码来查看它们。幸运的是,在EFCore中加载数据很简单,严重依赖LINQ方法来控制所需的字段。对于您的应用程序,您将在RecipeService上创建一个方法,该方法返回配方的摘要视图,包括RecipeId、Name和TimeToCook作为RecipeSummaryViewModel,如图12.12所示。

注意:从技术上讲,创建视图模型是一个UI问题,而不是一个业务逻辑问题。我直接从这里的RecipeService返回它们,主要是为了提醒大家,您不应该直接在Razor Pages中使用EF Core实体。

图12.12 调用Index.chtml Razor页面并查询数据库以检索RecipeSummaryViewModels列表。EFCore生成SQL以从数据库中检索必要的字段,并将它们映射到视图模型对象。

RecipeService中的GetRecipes方法在概念上很简单,并遵循查询EFCore数据库的常见模式,如图12.13所示。

图12.13 EF Core数据库查询的三个部分

EFCore使用流畅的LINQ命令链来定义要返回到数据库的查询。AppDataContext上的DbSet<Recipe>属性是一个IQueryable,因此您可以使用与其他IQueryableProvider一起使用的所有常见Select()和Where()子句。当您调用执行函数(如ToListAsync()、ToArrayAsync()、SingleAsync(()或它们的非异步兄弟)时,EF Core会将它们转换为SQL语句以查询数据库。

您还可以使用Select()扩展方法映射到实体以外的对象,作为SQL查询的一部分。通过只获取所需的列,您可以使用它高效地查询数据库。

清单12.7显示了获取RecipeSummaryViewModel列表的代码,遵循与图12.12相同的基本模式。它使用Where LINQ表达式过滤掉标记为已删除的配方,并使用Select子句映射到视图模型。ToListAsync()命令指示EF Core生成SQL查询,在数据库上执行它,并根据返回的数据构建RecipeSummaryViewModel。

清单12.7 在RecipeService中使用EF Core加载项目列表

public async Task<ICollection<RecipeSummaryViewModel>> GetRecipes()
{
    return await _context.Recipes  ////查询从DbSet属性开始。
        .Where(r => !r.IsDeleted)
        //EF Core将只查询正确映射视图模型所需的Recipe列。
        .Select(r => new RecipeSummaryViewModel
        {
            Id = r.RecipeId, 
            Name = r.Name,
            TimeToCook = $"{r.TimeToCook.TotalMinutes}mins"
        })
        .ToListAsync();  //这将执行SQL查询并创建最终视图模型。
}

请注意,在Select方法中,使用字符串插值将TimeToCook属性从TimeSpan转换为字符串:

TimeToCook = $"{r.TimeToCook.TotalMinutes}mins"

我之前说过EF Core将LINQ表达式系列转换为SQL,但这只是半真半假;EF Core无法或不知道如何将某些表达式转换为SQL。对于这些情况,例如在本例中,EFCore从DB中找到在客户端运行表达式所需的字段,从数据库中选择这些字段,然后在C#中运行表达式。这使您可以在不损害C#功能的情况下,结合数据库端评估的功能和性能。

警告:客户端评估功能强大且有用,但可能会引发问题。通常,如果查询需要危险的客户端评估,最新版本的EFCore会抛出异常。例如,包括如何避免这些问题,请参阅http://mng.bz/zxP6.

此时,您有一个记录列表,显示配方数据的摘要,因此下一步显然是加载单个记录的详细信息。

12.4.3 加载单个记录

对于大多数意图和目的,加载单个记录与加载记录列表相同。它们共享图12.13中看到的相同的公共结构,但在加载单个记录时,通常会使用Where子句并执行将数据限制为单个实体的命令。

清单12.8显示了按ID获取配方的代码,遵循与之前相同的基本模式(图12.12)。它使用Where()LINQ表达式将查询限制为单个配方,其中RecipeId==ID,并使用Select子句映射到RecipeDetailViewModel。SingleOrDefaultAsync()子句将导致EFCore生成SQL查询,在数据库上执行它,并构建视图模型。

注意:如果前面的Where子句返回多条记录,SingleOrDefaultAsync()将引发异常。

GetRecipeDetail(int id) { return await _context.Recipes //与之前一样,查询从DbSet属性开始。 .Where(x => x.RecipeId == id) //将查询限制为具有提供的id的配方。 //将配方映射到RecipeDetailViewModel。 .Select(x => new RecipeDetailViewModel { Id = x.RecipeId, Name = x.Name, Method = x.Method, //作为同一查询的一部分加载并映射链接的成分。 Ingredients = x.Ingredients .Select(item => new RecipeDetailViewModel.Item { Name = item.Name, Quantity = $"{item.Quantity} {item.Unit}" }) }) .SingleOrDefaultAsync(); //执行查询并将数据映射到视图模型。 }

注意,除了将Recipe映射到RecipeDetailViewModel之外,您还映射了Recipe的相关成分,就好像您正在直接处理内存中的对象一样。这是使用ORM的优点之一——您可以轻松地映射子对象,并让EFCore决定如何最好地构建底层查询以获取数据。

注意:默认情况下,EF Core会将其运行的所有SQL语句记录为LogLevel.Information事件,因此您可以很容易地看到针对数据库运行的查询。

我们的应用程序正在稳步发展;您可以创建新的食谱,在列表中查看所有食谱,并深入查看包含其成分和方法的单个食谱。不过,很快就会有人引入一个错别字,并希望更改他们的数据。为此,您必须在CRUD:update中实现U。

12.4.4 使用更改更新模型

当实体发生变化时更新它们通常是CRUD操作中最困难的部分,因为变量太多。图12.14概述了应用于配方应用程序的这一过程。

我不打算在这本书中处理关系方面的问题,因为这通常是一个复杂的问题,而如何处理它取决于数据模型的细节。相反,我将专注于更新Recipe实体本身的属性。

图12.14 更新实体包括三个步骤:使用EFCore读取实体,更新实体的属性,并在DbContext上调用SaveChangesAsync()生成SQL以更新数据库中的正确行。

对于web应用程序,当您更新实体时,通常会遵循图12.14中概述的步骤:

  1. 从数据库中读取实体。
  2. 修改实体的属性。
  3. 将更改保存到数据库。

您将在RecipeService上名为UpdateRecipe的方法中封装这三个步骤。此方法采用UpdateRecipeCommand参数,并包含更改Recipe实体的代码。

注意:与Create命令一样,您不会直接修改RazorPage中的实体,确保UI关注点与业务逻辑分离。

下面的列表显示RecipeService.UpdateRecipe方法,它更新Recipe实体。它执行我们之前定义的读取、修改和保存实体的三个步骤。我提取了代码,用新的值将配方更新为助手方法。

//Find由Recipes直接公开,并简化了通过id读取实体的过程。 //如果提供的id无效,配方将为空。 if(recipe == null) { throw new Exception("Unable to find the recipe"); } UpdateRecipe(recipe, cmd); //在配方实体上设置新值。 await _context.SaveChangesAsync(); //执行SQL以将更改保存到数据库。 } //用于在Recipe实体上设置新属性的助手方法 static void UpdateRecipe(Recipe recipe, UpdateRecipeCommand cmd) { recipe.Name = cmd.Name; recipe.TimeToCook = new TimeSpan(cmd.TimeToCookHrs, cmd.TimeToCookMins, 0); recipe.Method = cmd.Method; recipe.IsVegetarian = cmd.IsVegetarian; recipe.IsVegan = cmd.IsVegan; }

在本例中,我使用DbSet公开的FindAsync(id)方法读取Recipe实体。这是一个简单的助手方法,用于通过实体的ID(在本例中为RecipeId)加载实体。我本可以使用LINQ编写类似的查询

__context.Recipes.Where(r=>r.RecipeId == cmd.Id).FirstOrDefault();

使用FindAsync()或Find()更具声明性和简洁性。

提示:查找实际上有点复杂。查找第一个检查以查看实体是否已经在EF Core的DbContext中被跟踪。如果是(因为该实体先前已加载到此请求中),则立即返回该实体,而不调用DB。如果实体被跟踪,这显然会更快,但如果您知道实体尚未被跟踪,则速度也会更慢。

您可能想知道,当您调用SaveChangesAsync()时,EF Core如何知道要更新哪些列。最简单的方法是更新每一列——如果字段没有更改,那么再次写入相同的值也无所谓。但EF Core比这更聪明一点。

EF Core在内部跟踪它从数据库加载的任何实体的状态。它将创建实体的所有属性值的快照,以便跟踪哪些属性值已更改。当调用SaveChanges()时,EF Core会将任何被跟踪实体(在本例中为Recipe实体)的状态与跟踪快照进行比较。任何已更改的属性都包含在发送到数据库的UPDATE语句中,而未更改的属性将被忽略。

注意:EF Core提供了其他跟踪更改的机制,以及完全禁用更改跟踪的选项。有关详细信息,请参阅Jon P.Smith的《实体框架核心行动》第二版(Manning,2021)的文档或第3章:http://mng.bz/q9PJ.

有了更新食谱的功能,你的食谱应用程序几乎就完成了。“但等一下,”我听到你哭了,“我们还没有处理CRUD中的D——删除!”这是真的,但在现实中,我发现很少有人想删除数据。

让我们考虑一下从应用程序中删除配方的要求,如图12.15所示。你需要在食谱旁边添加一个(看起来吓人的)删除按钮。用户单击“删除”后,配方在列表中不再可见,无法查看。

图12.15 从应用程序中删除配方时的预期行为。单击“删除”将返回应用程序的主列表视图,删除的配方将不再可见。

您可以通过从数据库中删除配方来实现这一点,但数据的问题是,一旦它消失,它就消失了!如果用户不小心删除了记录怎么办?此外,从关系数据库中删除行通常会对其他实体产生影响。例如,由于Ingredient.RecipeId上的外键约束,您不能在应用程序中删除Recipe表中的行,而不删除引用该行的所有Ingredient行。

EF Core可以使用DbContext.Remote(entity)命令轻松处理这些真正的删除场景,但通常当您发现需要删除数据时,您的意思是将其“存档”或从UI中隐藏。处理这种情况的一种常见方法是在您的实体上添加某种“此实体是否已删除”标志,例如Recipe实体上包含的IsDeleted标志:

public bool IsDeleted {get;set;}

如果采用这种方法,删除数据会突然变得更简单,因为它只不过是对实体的更新。不再有丢失数据的问题,也不再有引用完整性问题。

注意:我发现这种模式的主要例外是当您存储用户的个人识别信息时。在这些情况下,您可能有义务(并且可能有法律义务)根据请求从您的数据库中删除他们的信息。

使用这种方法,您可以在RecipeService上创建一个delete方法来更新IsDeleted标志,如下面的列表所示。此外,您应该确保RecipeService中的所有其他方法中都有Where()子句,以确保无法显示已删除的Recipe,如清单12.9中GetRecipes()方法所示。

//按id获取配方实体。 //如果提供的id无效,配方将为空。 if(recipe is null) { throw new Exception("Unable to find the recipe"); } recipe.IsDeleted = true; //将配方标记为已删除。 await _context.SaveChangesAsync(); //执行SQL以将更改保存到数据库。 }

这种方法满足了需求——它从应用程序的UI中删除了配方——但它简化了许多事情。这种软删除方法不适用于所有场景,但我发现它是我所从事项目中的常见模式。

提示:EFCore有一个方便的功能,叫做全局查询过滤器。这些允许您在模型级别指定Where子句,因此,例如,您可以确保EF Core从不加载IsDeleted为true的Recipes。这对于在多租户环境中隔离数据也很有用。有关详细信息,请参阅文档:https://docs.microsoft.com/ef/core/querying/filters.

关于EF Core的本章即将结束。我们已经介绍了将EF Core添加到项目中并使用它简化数据访问的基本知识,但随着应用程序变得更加复杂,您可能需要了解更多有关EF Core的信息。在本章的最后一节中,我想指出在您自己的应用程序中使用EF Core之前需要考虑的一些事项,因此您熟悉随着应用程序的发展将面临的一些问题。

12.5 在生产应用中使用EF Core

这本书是关于ASP.NET Core,而不是EF Core,所以我不想花太多时间探索EF Core。本章应该已经为您提供了足够的准备和运行,但在考虑将EF Core投入生产之前,您肯定需要了解更多信息。正如我多次提到的,我推荐Jon P.Smith(Manning,2021)的《实体框架核心在行动》第二版,以了解详细信息(http://mng.bz/7Vme)或浏览EF Core文档网站https://docs.microsoft.com/ef/core/.

以下主题对于开始使用EF Core并不重要,但如果您构建了一个可用于生产的应用程序,您很快就会遇到这些主题。本节并不是解决这些问题的规定性指南;在投入生产之前,需要考虑更多的事情。

  • 列的脚手架——EF Core通过允许字符串长度较大或不受限制,对字符串列等使用保守值。实际上,您可能希望将这些和其他数据类型限制为合理的值。
  • 验证——您可以用DataAnnotations验证属性来修饰实体,但EFCore不会在保存到数据库之前自动验证值。这与EF6.x行为不同,其中验证是自动的。
  • 处理并发性——EF Core提供了几种处理并发性的方法,其中多个用户试图同时更新一个实体。一个部分解决方案是在实体上使用时间戳列。
  • 同步与异步——EF Core提供了用于与数据库交互的同步和异步命令。通常,异步对web应用程序更好,但这一论点存在细微差别,使得在任何情况下都不可能推荐一种方法而不是另一种方法。

EFCore是一个很好的工具,可以在编写数据访问代码时提高效率,但在使用数据库时,有些方面不可避免地会很尴尬。数据库管理问题是最棘手的问题之一。这本书是关于ASP.NET Core,而不是EF Core,所以我不想过多地讨论数据库管理。尽管如此,大多数web应用程序都使用某种数据库,因此以下问题可能会在某一时刻影响您:

  • 自动迁移——如果您将应用程序自动部署到生产环境,作为某种DevOps管道的一部分,那么您将不可避免地需要某种方式自动将迁移应用到数据库。您可以通过多种方式解决这一问题,例如编写.NET工具脚本、在应用程序的启动代码中应用迁移或使用自定义工具。每种方法都有其利弊。
  • 多个web主机——一个具体的考虑是,是否有多个web服务器托管您的应用程序,所有服务器都指向同一个数据库。如果是这样,那么在应用程序的启动代码中应用迁移将变得更加困难,因为您必须确保一次只能有一个应用程序迁移数据库。
  • 进行向后兼容的模式更改——多web主机方法的一个必然结果是,您的应用程序经常会访问一个数据库,该数据库的模式比应用程序想象的要新。这意味着您通常应尽可能使模式更改向后兼容。
  • 将迁移存储在不同的程序集中——在本章中,我将所有逻辑都包含在一个项目中,但在大型应用程序中,数据访问通常在不同于web应用程序的项目中。对于具有此结构的应用程序,在使用.NET CLI或PowerShell cmdlet时,必须使用稍微不同的命令。
  • 种子数据——当您第一次创建数据库时,通常希望它具有一些初始种子数据,例如默认用户。EF6.x内置了一种播种数据的机制,而EFCore要求您自己明确播种数据库。

如何选择处理这些问题取决于应用程序的基础架构和部署方法。它们都不是特别有趣,但它们是一种不幸的必需品。尽管如此,放心吧,它们都可以通过某种方式解决!

这让我们结束了关于EF Core的本章。在下一章中,我们将讨论MVC和Razor Pages中稍微高级一些的主题之一:过滤器管道,以及如何使用它来减少代码中的重复。

总结

  • EFCore是一个对象关系映射器(ORM),它允许您通过操作应用程序中的标准POCO类(称为实体)与数据库交互。这可以减少生产所需的SQL和数据库知识量。
  • EFCore将实体类映射到表,将实体上的属性映射到表中的列,将实体对象的实例映射到这些表中的行。即使您使用EFCore避免直接使用数据库,也需要记住这个映射。
  • EFCore使用一个数据库提供程序模型,允许您在不更改任何对象操作代码的情况下更改基础数据库。EF Core为Microsoft SQL Server、SQLite、PostgreSQL、MySQL和其他许多数据库提供程序。
  • EF Core是跨平台的,对于ORM具有良好的性能,但它具有与EF 6.x不同的特性集。然而,EF Core建议用于EF 6.x以上的所有新应用程序。
  • EF Core基于应用程序的DbContext上的DbSet<T>属性,存储应用程序中实体的内部表示以及它们如何映射到数据库。EF Core基于实体类本身及其引用的任何其他实体构建模型。
  • 通过添加NuGet数据库提供程序包,将EF Core添加到应用程序中。您还应该安装EF Core的设计包。这与.NET工具一起工作,以生成迁移并将迁移应用到数据库。
  • EF Core包含许多关于如何定义实体的约定,例如主键和外键。您可以使用DataAnnotations或使用流畅的API自定义实体的定义方式。
  • 应用程序使用DbContext与EF Core和数据库交互。使用AddDbContext<T>将其注册到DI容器中,定义数据库提供程序并提供连接字符串。这使您的DbContext在整个应用程序的DI容器中可用。
  • EF Core使用迁移来跟踪对实体定义的更改。它们用于确保实体定义、EFCore的内部模型和数据库模式都匹配。
  • 更改实体后,可以使用.NET工具或Visual Studio PowerShell cmdlet创建迁移。
  • 要使用.NET CLI创建新的迁移,请在项目文件夹中运行dotnet ef migrations add NAME,其中NAME是要为迁移命名的名称。这会将当前的DbContext快照与以前的版本进行比较,并生成更新数据库所需的SQL语句。
  • 您可以使用dotnet-ef数据库更新将迁移应用到数据库。如果数据库不存在,这将创建数据库,并应用任何未完成的迁移。
  • EF Core在创建迁移时不与数据库交互,只有在您显式更新数据库时才与数据库交互。因此,您仍然可以在脱机时创建迁移。
  • 您可以通过创建一个新实体(例如,调用_context.add(e)到应用程序的数据上下文(_context)实例上,并调用_context.SaveChangesAsync()),将实体添加到EF Core数据库中。这将生成必要的SQL INSERT语句,以将新行添加到数据库中。
  • 您可以使用应用程序的DbContext上的DbSet<T>属性从数据库加载记录。这些公开了IQueryable接口,因此您可以在返回数据之前使用LINQ语句过滤和转换数据库中的数据。
  • 更新实体包括三个步骤:从数据库中读取实体、修改实体和将更改保存到数据库。EF Core将跟踪哪些属性发生了更改,以便优化生成的SQL。
  • 您可以使用Remove方法删除EF Core中的实体,但您应该仔细考虑是否需要此功能。通常,在实体上使用IsDeleted标志的软删除技术更安全,更容易实现。
  • 本章仅涵盖在应用程序中使用EF Core时必须考虑的问题的子集。在生产应用程序中使用它之前,除其他事项外,您应该考虑为字段生成的数据类型、验证、如何处理并发、初始数据的种子、在运行的应用程序上处理迁移以及在web场场景中处理迁移。

(请点击这里阅读其他章节)