数据库访问代码在Web应用程序中无处不在。无论您是构建电子商务应用程序、博客还是NextBigThing,您都可能需要与数据库进行交互。
不幸的是,从应用程序代码与数据库交互通常是一件麻烦事,您可以采用许多不同的方法。例如,从数据库读取数据这样简单的事情需要处理网络连接、编写SQL语句和处理可变结果数据。.NET生态系统拥有一整套可用于此目的的库,从低级ADO.NET库到高级抽象,如EFCore。
在本节中,我将描述EFCore是什么以及它旨在解决的问题。我将介绍使用EFCore等抽象背后的动机,以及它如何帮助弥合应用程序代码和数据库之间的差距。作为其中的一部分,我将介绍您在应用程序中使用它时将做出的一些权衡,这将帮助您确定它是否适合您的目的。最后,我们将看一个示例EFCore映射,从应用程序代码到数据库,以了解EFCore的主要概念。
EFCore是一个库,它提供了一种面向对象的方式来访问数据库。它充当对象关系映射器(ORM),为您与数据库通信并将数据库响应映射到.NET类和对象,如图12.1所示。
定义使用对象关系映射器(ORM),您可以使用面向对象的概念(例如类和对象)来操作数据库,方法是将它们映射到数据库概念(例如表和列)。
EFCore基于但不同于现有的实体框架库(目前最高版本6.x)。它是作为.NETCore推动跨平台工作的一部分而构建的,但考虑到了其他目标。特别是,EFCore团队希望制作一个可与各种数据库一起使用的高性能库。
有许多不同类型的数据库,但可能最常用的系列是关系数据库,使用结构化查询语言(SQL)访问。这是EFCore的基础;它可以映射MicrosoftSQLServer、MySQL、Postgres和许多其他关系数据库。它甚至还有一个很酷的内存功能,您可以在测试时使用它来创建临时数据库。EFCore使用提供者模型,以便以后可以在其他关系数据库可用时插入对它们的支持。
这涵盖了EFCore是什么,但没有深入探讨您为什么要使用它。为什么不使用传统的ADO.NET库直接访问数据库?使用EFCore的大多数论点通常都可以应用于ORM,那么ORM的优势是什么?
ORM带来的最大优势之一是开发应用程序的速度。您可以留在熟悉的面向对象.NET领域,通常无需直接操作数据库或编写自定义SQL。
假设您有一个电子商务站点,并且您想从数据库中加载产品的详细信息。使用低级数据库访问代码,您必须打开与数据库的连接,使用正确的表名和列名编写必要的SQL,通过连接读取数据,创建POCO来保存数据,并手动设置对象的属性,随时将数据转换为正确的格式。听起来很痛苦,对吧?
诸如EFCore之类的ORM会为您处理大部分工作。它处理与数据库的连接、生成SQL并将数据映射回您的POCO对象。您只需要提供一个描述您要检索的数据的LINQ查询。
ORM充当数据库的高级抽象,因此它们可以显着减少与数据库交互所需编写的管道代码量。在最基本的层面上,它们负责将SQL语句映射到对象,反之亦然,但大多数ORM更进一步,并提供了额外的特性。
像EFCore这样的ORM会跟踪它们从数据库中检索到的任何对象的哪些属性发生了变化。这使您可以通过从数据库表映射对象来从数据库加载对象,在.NET代码中对其进行修改,然后要求ORM更新数据库中的关联记录。ORM将计算出哪些属性已更改并为相应的列发出更新语句,从而为您节省大量工作。
与软件开发中经常发生的情况一样,使用ORM也有其缺点。ORM的最大优势之一也是它们的致命弱点——它们对您隐藏了数据库。有时,这种高层次的抽象可能会导致您的应用程序中出现问题的数据库查询模式。一个经典的例子是N+1问题,其中应该是单个数据库请求变成了对数据库表中每一行的单独请求。
另一个常见的缺点是性能。ORM是对多个概念的抽象,因此与您在应用程序中手工制作每条数据访问相比,它们本质上会做更多的工作。大多数ORM,包括EFCore,都会牺牲一定程度的性能来简化开发。
也就是说,如果您意识到ORM的缺陷,您通常可以大大简化与数据库交互所需的代码。与任何事情一样,如果抽象对您有用,请使用它;否则,不要。如果您只有最低的数据库访问要求,或者您需要获得最佳性能,那么像EFCore这样的ORM可能不适合。
即使您决定在应用程序中使用ORM,也有许多可用于.NET的不同ORM,EFCore就是其中之一。EFCore是否适合您将取决于您需要的功能以及您愿意为获得这些功能而做出的权衡。下一部分将EFCore与Microsoft的其他产品实体框架进行比较,但您可以考虑许多其他替代方案,例如Dapper和NHibernate,每个都有自己的权衡取舍。
Microsoft将EFCore设计为对2008年发布的成熟EntityFramework6.x(EF6.x)ORM的重新构想。经过十年的发展,EF6.x是一个稳定且功能丰富的ORM。
相比之下,EFCore是一个相对较新的项目。EFCore的API设计为接近EF6.x的API(尽管它们并不完全相同),但核心组件已被完全重写。您应该将EFCore视为与EF6.x不同的版本;直接从EF6.x升级到EFCore并非易事。
Microsoft支持EFCore和EF6.x,两者都将看到持续改进,那么您应该选择哪一个?你需要考虑很多事情:
这些权衡和限制是否对您来说是一个问题,很大程度上取决于您的特定应用程序。考虑到这些限制,启动一个新的应用程序比稍后尝试解决它们要容易得多。
如果你正在开发一个新的ASP.NETCore应用程序,你想使用ORM进行快速开发,并且你不需要任何不可用的功能,那么EFCore是一个很好的竞争者。ASP.NETCore的各种其他子系统也开箱即用地支持它。例如,在第14章中,您将了解如何使用EFCore和ASP.NETCore身份验证系统来管理应用程序中的用户。
在深入了解在应用程序中使用EFCore的细节之前,我将描述我们将用作本章案例研究的应用程序。我们将介绍应用程序和数据库的详细信息以及如何使用EFCore在两者之间进行通信。
EFCore专注于应用程序和数据库之间的通信,因此为了展示它,我们需要一个应用程序。本章以一个简单的烹饪应用程序为例,它列出了食谱并让您查看食谱的成分,如图12.2所示。用户可以浏览食谱、添加新食谱、编辑食谱和删除旧食谱。
这显然是一个简单的应用程序,但它包含了您需要的所有数据库交互,其中包括两个实体:Recipe和Ingredient。
定义实体是由EFCore映射到数据库的.NET类。这些是您定义的类,通常作为POCO类,可以通过使用EFCore映射到数据库表来保存和加载。
与EFCore交互时,您将主要使用POCO实体和从DbContextEFCore类继承的数据库上下文。实体类是数据库中表的面向对象的表示;它们代表您要存储在数据库中的数据。您可以在应用程序中使用DbContext来配置EFCore并在运行时访问数据库。
注意您的应用程序中可能有多个DbContext,甚至可以将它们配置为与不同的数据库集成。
当您的应用程序首次使用EFCore时,EFCore会根据应用程序的DbContext上的DbSet
对于您的食谱应用,EFCore将构建Recipe类的模型,因为它在AppDbContext上作为DbSet
每个实体都映射到数据库中的一个表,但EFCore还映射实体之间的关系。每个菜谱可以有很多成分,但每个成分(有名称、数量和单位)属于一个菜谱,所以这是一个多对一的关系。EFCore使用该知识正确建模等效的多对一数据库结构。
注意两种不同的食谱,比如鱼派和柠檬鸡,可能使用名称和数量相同的成分,例如一个柠檬的汁,但它们从根本上是两个不同的实例。如果您将柠檬鸡食谱更新为使用两个柠檬,您不会希望此更改自动更新鱼饼以使用两个柠檬!
EFCore在与数据库交互时使用它构建的内部模型。这可确保它构建正确的SQL来创建、读取、更新和删除实体。
是的,是时候写一些代码了!在下一部分中,您将开始构建食谱应用程序。您将了解如何将EFCore添加到ASP.NETCore应用程序、配置数据库提供程序以及设计应用程序的数据模型。
在本节中,我们将重点介绍在您的ASP.NETCore配方应用中安装和配置EFCore。您将学习如何安装所需的NuGet包以及如何为您的应用程序构建数据模型。当我们在本章中讨论EFCore时,我不会大体上介绍如何创建应用程序——我创建了一个简单的RazorPages应用程序作为基础,没什么特别的。
将EFCore添加到应用程序是一个多步骤过程:
1选择数据库提供商;例如,Postgres、SQLite或MSSQLServer。2安装EFCoreNuGet包。3设计应用程序的DbContext和构成数据模型的实体。4将应用程序的DbContext注册到ASP.NETCoreDI容器。
5使用EFCore生成描述数据模型的迁移。6将迁移应用到数据库以更新数据库的架构。
设置EFCore的第一步是决定要与哪个数据库进行交互。客户或您公司的政策可能会向您规定这一点,但仍然值得考虑一下选择。
EFCore使用提供程序模型支持一系列数据库。EFCore的模块化特性意味着您可以使用相同的高级API对不同的底层数据库进行编程,并且EFCore知道如何生成必要的特定于实现的代码和SQL语句。
当您启动应用程序时,您可能已经想到了一个数据库,并且您会很高兴知道EFCore已经涵盖了大多数流行的数据库。添加对给定数据库的支持涉及将正确的NuGet包添加到.csproj文件。例如,
一些数据库提供程序包由Microsoft维护,一些由开源社区维护,还有一些可能需要付费许可证(例如,Oracle提供程序),因此请务必检查您的要求。
以与任何其他库相同的方式将数据库提供程序安装到应用程序中:通过将NuGet包添加到项目的.csproj文件并从命令行运行dotnetrestore(或让VisualStudio自动为您恢复)。
EFCore本质上是模块化的,因此您需要安装多个包。我将SQLServer数据库提供程序与LocalDB一起用于配方应用程序,因此我将使用SQLServer包:
清单12.1显示了添加EFCore包后配方应用的.csproj文件。请记住,您将NuGet包添加为PackageReference元素。
清单12.1将EFCore安装到ASP.NETCore应用程序中
在第12.1.4节中,我概述了EFCore如何从DbContext和实体模型构建数据库的内部模型。除了这种发现机制之外,EFCore在让您以您想要的方式定义实体方面非常灵活,例如POCO类。
一些ORM要求您的实体从特定的基类继承,或者您使用属性装饰模型以描述如何映射它们。正如您在此清单中所见,EFCore非常偏爱约定而不是配置方法,该清单显示了您的应用程序的配方和成分实体类。
清单12.2定义EFCore实体类
publicclassRecipe{publicintRecipeId{get;set;}publicstringName{get;set;}publicTimeSpanTimeToCook{get;set;}publicboolIsDeleted{get;set;}publicstringMethod{get;set;}publicICollection
定义表的主键是在表中所有其他行中唯一标识该行的值。它通常是int或Guid。
此处可见的另一个约定是成分类的RecipeId属性。EFCore将其解释为指向Recipe类的外键。当与Recipe类上的ICollection
定义表上的外键指向不同表的主键,形成两行之间的链接。
提示您还可以使用DataAnnotations属性来装饰您的实体类,控制列命名或字符串长度等内容。EFCore将使用这些属性来覆盖默认约定。
除了实体之外,您还可以为应用程序定义DbContext。这是应用程序中EFCore的核心,用于所有数据库调用。创建一个自定义DbContext,在本例中称为AppDbContext,并从DbContext基类派生,如下所示。这会公开DbSet
清单12.3定义应用程序DbContext
publicclassAppDbContext:DbContext{//构造函数选项对象,包含连接字符串等详细信息publicAppDbContext(DbContextOptions
注意您没有在AppDbContext上列出成分,但它将由EFCore建模,因为它在配方中公开。您仍然可以访问数据库中的成分对象,但您必须通过食谱实体的成分属性导航才能这样做,如您将在第12.4节中看到的那样。
对于这个简单的示例,您的数据模型由以下三个类组成:AppDbContext、Recipe和Ingredient。这两个实体将映射到表,它们的列映射到属性,您将使用AppDbContext来访问它们。
数据模型已经完成,但您还没有准备好使用它。您的ASP.NETCore应用程序不知道如何创建您的AppDbContext,并且您的AppDbContext需要一个连接字符串,以便它可以与数据库通信。在下一节中,我们将解决这两个问题,并完成在您的ASP.NETCore应用程序中设置EFCore。
与ASP.NetCore中的任何其他服务一样,您应该将AppDbContext注册到DI容器。注册上下文时,您还可以配置数据库提供程序并设置连接字符串,以便EFCore知道如何与数据库通信。
您在Startup.cs的ConfigureServices方法中注册AppDbContext。EFCore为此提供了一个通用的AddDbContext
再次,您的应用程序的配置既漂亮又简单,如下面的清单所示。您使用Microsoft.EntityFrameworkCore.SqlServer包提供的UseSqlServer扩展方法设置数据库提供程序,并将连接字符串传递给它。
清单12.4向DI容器注册一个DbContext
publicvoidConfigureServices(IServiceCollectionservices){//连接字符串取自配置,来自ConnectionStrings部分。varconnString=Configuration.GetConnectionString("DefaultConnection");//将应用程序的DbContext用作通用参数来注册它。services.AddDbContext
正如我在前一章中讨论的那样,连接字符串是一个典型的秘密,因此从配置中加载它是有意义的。在运行时,将使用当前环境的正确配置字符串,因此您可以在本地开发和生产环境中使用不同的数据库。
提示您可以通过其他方式配置您的AppDbContext并提供连接字符串,例如使用OnConfiguring方法,但我建议ASP.NETCore网站使用此处显示的方法。
您现在有一个在DI容器中注册的DbContext、AppDbContext,以及一个与您的数据库对应的数据模型。就代码而言,您已准备好开始使用EFCore,但您还没有数据库!在下一部分中,您将了解如何轻松使用.NETCLI来确保您的数据库与您的EFCore数据模型保持同步。
在本节中,您将学习如何使用迁移生成SQL语句,以使数据库的架构与应用程序的数据模型保持同步。您将学习如何创建初始迁移并使用它来创建数据库。然后,您将更新您的数据模型,创建第二个迁移,并使用它来更新数据库模式。
管理数据库的模式更改(例如当您需要添加新表或新列时)非常困难。您的应用程序代码明确绑定到特定版本的数据库,您需要确保两者始终保持同步。
定义模式是指数据在数据库中的组织方式,包括表、列以及它们之间的关系等。
部署应用程序时,您通常可以删除旧代码/可执行文件并用新代码替换它——工作完成。如果您需要回滚更改,请删除该新代码并部署旧版本的应用程序。
数据库的困难在于它们包含数据!这意味着不可能在每次部署时都把它吹走并创建一个新数据库。
一个常见的最佳实践是显式地对数据库的模式和应用程序的代码进行版本控制。您可以通过多种方式执行此操作,但通常需要存储数据库的先前模式和新模式之间的差异,通常作为SQL脚本。然后,您可以使用DbUp和FluentMigrator等库来跟踪已应用的脚本并确保您的数据库架构是最新的。或者,您可以使用外部工具为您进行管理。
您可以使用命令行工具从迁移中创建新数据库,或者通过向现有数据库应用新迁移来更新现有数据库。您甚至可以回滚迁移,这会将数据库更新到以前的模式。
警告应用迁移会修改数据库,因此您必须始终注意数据丢失。如果您使用迁移从数据库中删除一个表,然后回滚迁移,则该表将被重新创建,但它之前包含的数据将永远消失!
在本节中,您将看到如何创建第一个迁移并使用它来创建数据库。然后您将更新您的数据模型,创建第二个迁移,并使用它来更新数据库架构。
在创建迁移之前,您需要安装必要的工具。有两种主要方法可以做到这一点:
在本书中,我将使用跨平台的.NET工具,但如果您熟悉EF6.x或者更喜欢使用VisualStudioPMC,那么您将执行的所有步骤都有等效的命令5你可以通过运行dotnetef来检查.NET工具是否安装正确。这应该会产生一个如图12.6所示的帮助屏幕。
提示如果您在运行上述命令时收到“Noexecutablefoundmatchingcommand‘dotnet-ef’”消息,请确保您已使用dotnettoolinstall--globaldotnet-ef安装了全局工具。通常,您需要从已注册AppDbContext的项目文件夹中运行dotnetef工具(而不是在解决方案文件夹级别)。
安装工具并配置数据库上下文后,您可以通过在Web项目文件夹中运行以下命令并提供迁移名称来创建第一个迁移-在本例中为InitialSchema:
dotnetefmigrationsaddInitialSchema此命令在项目的Migrations文件夹中创建三个文件:
EFCore可以使用AppDbContextModelSnapshot.cs在创建新迁移时确定数据库的先前状态,而无需直接与数据库交互。
这三个文件封装了迁移过程,但添加迁移不会更新数据库本身的任何内容。为此,您必须运行不同的命令才能将迁移应用到数据库。
提示在运行以下命令之前,您可以并且应该查看EFCore生成的迁移文件以检查它将对您的数据库执行的操作。安全总比抱歉!
您可以通过以下三种方式之一应用迁移:
哪个最适合您取决于您如何设计应用程序、如何更新生产数据库以及您的个人偏好。我现在将使用.NET工具,但我将在12.5节中讨论其中的一些注意事项。
您可以通过运行将迁移应用到数据库
dotnetefdatabaseupdate从应用程序的项目文件夹中。我不会详细介绍它是如何工作的,但是这个命令执行四个步骤:
1构建您的应用程序。2加载在应用的Startup类中配置的服务,包括AppDbContext。3检查AppDbContext连接字符串中的数据库是否存在。如果没有,它会创建它。4通过应用任何未应用的迁移来更新数据库。
如果一切配置正确,正如我在第12.2节中所展示的,那么运行此命令将为您设置一个闪亮的新数据库,如图12.7所示。
注意如果您在运行这些命令时收到“未找到项目”的错误消息,请检查您是否在应用程序的项目文件夹中运行它们,而不是在顶级解决方案文件夹中。
将迁移应用到数据库时,EFCore会在数据库中创建必要的表并添加适当的列和键。您可能还注意到__EFMigrationsHistory表。EFCore使用它来存储它应用于数据库的迁移的名称。下次您运行dotnetefdatabaseupdate时,EFCore可以将此表与您的应用程序中的迁移列表进行比较,并且只会将新的迁移应用到您的数据库。
在下一节中,我们将了解这如何使更改数据模型和更新数据库模式变得容易,而无需从头开始重新创建数据库。
大多数应用程序不可避免地会发展,无论是由于范围的扩大还是简单的维护。向实体添加属性、完全添加新实体以及删除过时的类——所有这些都是可能的。
EFCore迁移使这变得简单。想象一下,您决定通过在Recipe实体上公开IsVegetarian和IsVegan属性来突出显示食谱应用程序中的素食和纯素食菜肴。将您的实体更改为您想要的状态,生成迁移,并将其应用于数据库,如图12.8所示。
清单12.5向Recipe实体添加属性
publicclassRecipe{publicintRecipeId{get;set;}publicstringName{get;set;}publicTimeSpanTimeToCook{get;set;}publicboolIsDeleted{get;set;}publicstringMethod{get;set;}publicboolIsVegetarian{get;set;}publicboolIsVegan{get;set;}publicICollection
dotnetefmigrationsaddExtraRecipeFields这会通过添加迁移文件及其.designer.cs快照文件并更新AppDbContextModelSnapshot.cs在您的项目中创建第二个迁移,如图12.9所示。
和以前一样,这会创建迁移文件,但不会修改数据库。您可以通过运行应用迁移并更新数据库
dotnetefdatabaseupdate这会将应用程序中的迁移与数据库中的__EFMigrationsHistory表进行比较,以查看哪些迁移未完成,然后运行它们。EFCore将运行20200511204457_ExtraRecipeFields迁移,将IsVegetarian和IsVegan字段添加到数据库中,如图12.10所示。
当您单独工作或部署到单个Web服务器时,迁移很容易使用,但即使在这些情况下,在决定如何管理数据库时也需要考虑一些重要的事情。对于具有多个使用共享数据库的Web服务器的应用程序,或者对于容器化应用程序,您需要考虑的事情甚至更多。
这本书是关于ASP.NETCore,而不是EFCore,所以我不想过多地讨论数据库管理,但是第12.5节指出了在生产中使用迁移时需要牢记的一些事情。
在下一节中,我们将回到实质内容——定义我们的业务逻辑并在数据库上执行CRUD操作。
让我们回顾一下您在创建配方应用程序时所处的位置:
在本节中,您将通过创建一个RecipeService来为您的应用程序构建业务逻辑。这将处理查询数据库的食谱、创建新食谱和修改现有食谱。由于这个应用程序只有一个简单的域,我将使用RecipeService来处理所有需求,但在您自己的应用程序中,您可能有多个服务协同提供业务逻辑。
注意对于简单的应用程序,您可能会想将此逻辑移动到您的Razor页面中。我鼓励你抵制这种冲动;将业务逻辑提取到其他服务可以将Razor页面和WebAPI的以HTTP为中心的性质与底层业务逻辑分离。这通常会使您的业务逻辑更容易测试和更可重用。
我们的数据库中还没有任何数据,所以我们最好先让您创建一个食谱。
在本节中,您将构建让用户在应用程序中创建食谱的功能。这将主要包含一个表单,用户可以使用该表单使用RazorTagHelpers输入配方的所有详细信息,您在第7章和第8章中了解了该表单。该表单发布到Create.cshtmlRazor页面,该页面使用模型绑定和验证属性来确认请求是有效的,正如你在第6章中看到的那样。
在这个应用程序中创建配方的业务逻辑很简单——没有逻辑!将Create.cshtmlRazorPage中提供的命令绑定模型映射到Recipe实体及其成分,将Recipe对象添加到AppDbContext,并将其保存在数据库中,如图12.11所示。
在EFCore中创建实体涉及向映射表添加新行。对于您的应用程序,每当您创建新配方时,您还会添加链接的成分实体。EFCore负责通过为数据库中的每个成分创建正确的RecipeId来正确链接所有这些。
此示例所需的大部分代码涉及从CreateRecipeCommand转换为Recipe实体-与AppDbContext的交互仅包含两个方法:Add()和SaveChangesAsync()。
清单12.6在数据库中创建一个Recipe实体
readonlyAppDbContext_context;//使用DI将AppDbContext的一个实例注入到类构造函数中。//CreateRecipeCommand从RazorPage处理程序传入。publicasyncTask
1创建Recipe和Ingredient实体。2使用_context.Add(entity)将实体添加到EFCore的跟踪实体列表中。3通过调用_context.SaveChangesAsync()对数据库执行SQLINSERT语句,将必要的行添加到Recipe和Ingredient表中。
提示大多数涉及与数据库交互的EFCore命令都有同步和异步版本,例如SaveChanges()和SaveChangesAsync()。通常,异步版本将允许您的应用程序处理更多并发连接,因此我倾向于在可以使用它们时偏爱它们。
如果EFCore尝试与您的数据库交互时出现问题(例如,您尚未运行迁移来更新数据库架构),它将引发异常。我没有在这里展示它,但是在你的应用程序中处理这些是很重要的,这样当出现问题时你就不会向用户展示一个丑陋的错误页面。
假设一切顺利,EFCore会更新您的实体的所有自动生成的ID(RecipeId上的RecipeId,以及成分上的RecipeId和IngredientId)。将配方ID返回到Razor页面,以便它可以使用它;例如,重定向到查看食谱页面。
至此,您已经使用EFCore创建了您的第一个实体。在下一节中,我们将研究从数据库中加载这些实体,以便您可以在列表中查看它们。
对于大多数意图和目的,加载单个记录与加载记录列表相同。它们共享您在图12.13中看到的相同结构,但是在加载单个记录时,您通常会使用Where子句并执行将数据限制为单个实体的命令。
清单12.8显示了通过ID获取配方的代码,遵循与之前相同的基本模式(图12.12)。它使用Where()LINQ表达式将查询限制为单个配方,其中RecipeId==id,并使用Select子句映射到RecipeDetailViewModel。SingleOrDefaultAsync()子句将导致EFCore生成SQL查询,在数据库上执行它,并构建视图模型。
注意如果前面的Where子句返回多条记录,SingleOrDefaultAsync()将引发异常。
清单12.8在RecipeService中使用EFCore加载单个项目
注意EFCore默认将其运行的所有SQL语句记录为LogLevel.Information事件,因此您可以轻松查看针对数据库运行的查询。
我们的应用程序肯定正在成形;您可以创建新食谱,在列表中查看它们,并深入查看各个食谱及其成分和方法。不过,很快就会有人输入一个错字并想要更改他们的数据。为此,您必须在CRUD中实现U:update。
当实体发生变化时更新它们通常是CRUD操作中最难的部分,因为有很多变量。图12.14概述了此过程,因为它适用于您的食谱应用程序。
我不会在本书中处理关系方面,因为这通常是一个复杂的问题,你如何处理它取决于你的数据模型的细节。相反,我将专注于更新Recipe实体本身的属性。
对于Web应用程序,当您更新实体时,通常会遵循图12.14中概述的步骤:
1从数据库中读取实体。2修改实体的属性。3将更改保存到数据库。
您将把这三个步骤封装在一个名为UpdateRecipe的RecipeService方法中。此方法采用UpdateRecipeCommand参数并包含更改配方实体的代码。
下面的清单显示了RecipeService.UpdateRecipe方法,它更新了Recipe实体。它执行我们之前定义的三个步骤来读取、修改和保存实体。我已提取代码以将新值更新为辅助方法。
清单12.9在RecipeService中使用EFCore更新现有实体
publicasyncTaskUpdateRecipe(UpdateRecipeCommandcmd){//Find由Recipes直接公开,并简化了通过id读取实体的过程。varrecipe=await_context.Recipes.FindAsync(cmd.Id);//如果提供的id无效,recipe将为空。if(recipe==null){thrownewException("Unabletofindtherecipe");}//在配方实体上设置新值。UpdateRecipe(recipe,cmd);//执行SQL以保存对数据库的更改。await_context.SaveChangesAsync();}//用于在配方实体上设置新属性的辅助方法staticvoidUpdateRecipe(Reciperecipe,UpdateRecipeCommandcmd){recipe.Name=cmd.Name;recipe.TimeToCook=newTimeSpan(cmd.TimeToCookHrs,cmd.TimeToCookMins,0);recipe.Method=cmd.Method;recipe.IsVegetarian=cmd.IsVegetarian;recipe.IsVegan=cmd.IsVegan;}在此示例中,我使用DbSet公开的FindAsync(id)方法读取了Recipe实体。这是一个简单的帮助方法,用于通过ID加载实体,在本例中为RecipeId。我本可以使用LINQ编写一个类似的查询
提示Find实际上有点复杂。查找第一个检查以查看实体是否已在EFCore的DbContext中进行跟踪。如果是(因为之前在此请求中加载了实体),则立即返回实体而不调用DB。如果实体被跟踪,这显然会更快,但如果你知道实体还没有被跟踪,它也可能会更慢。
您可能想知道当您调用SaveChangesAsync()时EFCore如何知道要更新哪些列。最简单的方法是更新每一列——如果字段没有改变,那么再次写入相同的值也没关系。但是EFCore比这要聪明一些。
EFCore在内部跟踪它从数据库加载的任何实体的状态。它创建所有实体属性值的快照,因此它可以跟踪哪些属性值已更改。当您调用SaveChanges()时,EFCore会将任何跟踪实体(在本例中为配方实体)的状态与跟踪快照进行比较。任何已更改的属性都包含在发送到数据库的UPDATE语句中,而未更改的属性将被忽略。
有了更新食谱的能力,您的食谱应用程序就差不多完成了。“但是等等,”我听到你哭了,“我们还没有处理CRUD中的D——删除!”这是真的,但实际上,我发现很少有你想删除数据的情况。
让我们考虑从应用程序中删除配方的要求,如图12.15所示。您需要在食谱旁边添加一个(看起来很吓人的)删除按钮。用户单击删除后,该配方在列表中不再可见,也无法查看。
您可以通过从数据库中删除配方来实现这一点,但数据的问题是一旦它消失了,它就消失了!如果用户不小心删除了一条记录怎么办?此外,从关系数据库中删除一行通常会对其他实体产生影响。例如,由于外键,您不能在不删除所有引用它的成分行的情况下从应用程序中的配方表中删除一行对Ingredient.RecipeId的约束。
EFCore可以使用DbContext.Remove(entity)命令轻松地为您处理这些真正的删除场景,但通常当您发现需要删除数据时,您的意思是“归档”它或从UI中隐藏它。处理这种情况的常用方法是在您的实体上包含某种“此实体是否已删除”标志,例如我在Recipe实体中包含的IsDeleted标志:
publicboolIsDeleted{get;set;}如果采用这种方法,删除数据会突然变得更简单,因为它只不过是对实体的更新。不再有丢失数据的问题,也不再存在参照完整性问题。
注意我发现这种模式的主要例外是当您存储用户的个人识别信息时。在这些情况下,您可能有义务(并且可能有法律义务)根据要求从您的数据库中清除他们的信息。
使用这种方法,您可以在RecipeService上创建一个更新IsDeleted标志的删除方法,如下面的清单所示。此外,您应该确保在您的RecipeService中的所有其他方法中都有Where()子句,以确保您不能显示已删除的食谱,如清单12.9中的GetRecipes()方法所示。
清单12.10在EFCore中将实体标记为已删除
publicasyncTaskDeleteRecipe(intrecipeId){//通过id获取Recipe实体。varrecipe=await_context.Recipes.FindAsync(recipeId);//如果提供的id无效,recipe将为空。if(recipeisnull){thrownewException("Unabletofindtherecipe");}//将食谱标记为已删除。recipe.IsDeleted=true;//执行SQL以保存对数据库的更改。await_context.SaveChangesAsync();}这种方法满足了要求——它从应用程序的UI中删除了配方——但它简化了许多事情。这种软删除方法并不适用于所有场景,但我发现它是我从事的项目中的一种常见模式。
关于EFCore的这一章快要结束了。我们已经介绍了将EFCore添加到您的项目并使用它来简化数据访问的基础知识,但是随着您的应用程序变得越来越复杂,您可能需要了解有关EFCore的更多信息。在本章的最后一节中,我想指出在您自己的应用程序中使用EFCore之前需要考虑的一些事项,以便您熟悉您在应用程序中将面临的一些问题。
以下主题对于开始使用EFCore不是必需的,但如果您构建生产就绪的应用程序,您很快就会遇到它们。本节不是解决这些问题的说明性指南;在您投入生产之前,更多的是要考虑的一组事情。
EFCore是在编写数据访问代码时提高工作效率的好工具,但在使用数据库的某些方面不可避免地会很尴尬。数据库管理问题是最棘手的问题之一。这本书是关于ASP.NETCore,而不是EFCore,所以我不想过多地讨论数据库管理。话虽如此,大多数Web应用程序都使用某种数据库,因此以下问题可能会在某些时候影响您:
您选择如何处理这些问题将取决于您对应用程序采用的基础架构和部署方法。解决它们都不是特别有趣,但不幸的是它们是必需的。不过,振作起来,它们都可以通过一种或另一种方式解决!
这将我们带到了关于EFCore的本章的结尾。在下一章中,我们将看看MVC和RazorPages中稍微高级一点的主题:过滤器管道,以及如何使用它来减少代码中的重复。