Entity Framework 基础知识





5.00/5 (22投票s)
Entity Framework 的基础知识
引言
开发者们(包括我自己在内)一直以来都苦于如何将数据源中的数据映射到代码对象。然后,ORM(对象关系映射)出现了。ORM 代表 **o**bject-**r**elational **m**apping(对象关系映射),它使我们能够处理领域对象和属性。映射的难题就此结束。我记得以前我经常在代码中使用 Automapper 和 nHibernate。然后,微软在 2008 年发布了 Entity Framework。
照例,新发布的框架(基于 ADO.NET 构建)并没有被广泛接受。它有很多 bug,而且并没有达到预期。2010 年,微软发布了一个新版本,情况有所好转。他们一直在不断改进,现在它已经成为使用微软编程语言(C#、VB 等)时最常见的 ORM 之一。当前版本是 Entity Framework Core 7.0。
正如我之前所说,Entity Framework 是基于 ADO.NET 构建的,但它还提供了额外的选项。例如,映射是一个非常有用的功能。许多方面(尤其是错误)都可以追溯到 ADO.NET。了解一点 ADO 的知识是一个好习惯,这样你就能知道 Entity Framework 的底层是如何工作的。
在使用 Entity Framework 时,实体(Entities)是一个重要的概念。不是因为它在框架的名称中,而是因为它的工作方式。实体通常是代码中的一个对象,代表数据源中的一个实体。我喜欢说它代表数据库中的一个表。实体的属性在数据库中用作列和设置。例如:如果你有一个电影实体,它有 Id
(int
)、Title
(string
)以及可能的 Rating
(int
)。Entity Framework 可以将这些翻译成一个 MSSQL 表,其中包含 Id
(int
)、Title
(varchar(500)
)和 Rating
(int
)列。
但 Entity Framework 也可以将数据库表中的数据映射回代码中的对象(实体),反之亦然。我将在本教程《Entity Framework 入门》中展示这是如何工作的。
在本文的其余部分,我将使用缩写 EF 来表示 Entity Framework。
Code First 与 Database First
EF 有两种管理数据库的方式。在本教程中,我将只解释其中一种:Code First。另一种是 Database First。它们之间有很大区别,但 Code First 是最常用的。但在我们深入之前,我想解释这两种方法。
当数据库已经存在并且数据库不被代码管理时,就使用 Database First。当不存在当前数据库,但你想要创建一个时,就使用 Code First。
我更喜欢 Code First,因为我可以编写实体(本质上是带有属性的类),然后让 EF 相应地更新数据库。这只是 C#,我不用太担心数据库。我可以创建一个类,告诉 EF 这是一个实体,更新数据库,一切就都搞定了!
Database First 是反过来的。你让数据库“决定”你得到什么样的实体。你先创建数据库,然后相应地创建你的代码。
如前所述,本教程是关于 Code First 的。
开始之前...
在我们开始 Entity Framework 之前,我想从一个没有任何 Entity Framework 或数据库的简单控制台应用程序开始。我已经创建了一个可以从 GitHub 下载的应用程序。
快速了解一下这个应用程序。它不是什么高端应用——只是一个简单的控制台应用程序,带有基本菜单结构。如果你启动应用程序,可以看到所有电影,查看电影详情,还可以创建电影。
program.cs 文件并不特别。它只是电影的一个表示。一个实际的前端文件。我们不会改变 program.cs 的任何内容。我们将要处理的文件是 MovieService.cs。
internal class MovieService
{
private readonly List<movie> _movies = new()
{
new Movie{ Id = 1, Rating = 10, Title = "Shrek"},
new Movie{ Id = 2, Rating = 2, Title = "The Green Latern"},
new Movie{ Id = 3, Rating = 7, Title = "The Matrix"},
new Movie{ Id = 4, Rating = 4, Title = "Inception"},
new Movie{ Id = 5, Rating = 8, Title = "The Avengers"},
};
public IEnumerable<movie> GetAll()
{
return _movies.OrderBy(x => x.Title);
}
public Movie? Get(int id)
{
return _movies?.SingleOrDefault(x => x.Id == id);
}
public void Insert(Movie movie)
{
if (string.IsNullOrEmpty(movie.Title))
throw new Exception("Title is required.");
movie.Id = _movies.Max(x => x.Id) + 1;
_movies.Add(movie);
}
public void Delete(int id)
{
Movie? toDelete = Get(id);
if (toDelete != null)
_movies.Remove(toDelete);
}
}
MovieService.cs 包含了电影的所有逻辑。它与 Movie.cs 对象紧密协作,该对象拥有电影的所有属性。这个类允许我们获取所有电影、按 ID 获取单个电影、创建电影以及删除电影。这是一个典型的服务类。
在顶部,你会看到一个 private
变量,名为 _movies
。这是一个包含所有电影的列表。这些电影是硬编码的,你不会在任何数据源中找到它们。在本教程结束时,这些电影将存储在 MSSQL 数据库中,而 public
方法将读取和写入同一数据库的数据。
我们开始吧!
背景
使用 Entity Framework,一切都始于一个上下文。它将实体和关系与实际数据库关联起来。Entity Framework 带有 DbContext
,这是我们将在代码中使用的上下文。使用 DbContext
,你可以从数据库读取和写入数据,跟踪对象所做的更改,等等。我不会涵盖所有主题,只讲基础知识。
DbContext 类
我们需要做的第一件事是创建一个上下文类。这只是一个普通的 C# 类,继承自 DbContext
。DbContext
是 Entity Framework 的一个类。你需要安装一个包才能使用它:Microsoft.EntityFrameworkCore。这个包包含了我们需要的所有类和对象。
让我们创建一个新类,并称之为“MovieContext.cs”。继承 DbContext
并安装 NuGet 包 Microsoft.EntityFrameworkCore
。该类如下所示:
using Microsoft.EntityFrameworkCore;
namespace EntityFrameworkForDummies.ConsoleApp
{
internal class MovieContext : DbContext
{
}
}
要安装该包,你可以按 **Ctrl** + 。当光标停在 DbContext
上时,让 Visual Studio 安装最新版本,或者你可以使用程序包管理器,或者在程序包管理器控制台中执行以下命令:
Install-Package Microsoft.EntityFrameworkCore -Version 6.0.8
在这个类中,我们现在要做的就是告诉 DbContext
使用哪个 MSSQL 服务器。这可以是一个本地服务器、云服务器或其他服务器。EF 需要一个连接字符串来知道使用哪个服务器。我将使用一个本地数据库,连接字符串看起来是这样的:
Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=efdordummies;
Integrated Security=True;Connect Timeout=30;Encrypt=False;
TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False
Data Source 是服务器的位置。在我的例子中,它是 (localdb)\MSSQLLocalDb,它代表了 Visual Studio 自带的本地数据库。
Initial Catalog 是数据库的名称。给它一个好的、有意义的名称。有了 EF,就不必自己创建数据库了;如果需要,EF 会为你完成。
目前,这是最重要的两个设置。我不会详细介绍其他选项。
EF 需要知道连接 string
,我们可以通过几种方式将其提供给 EF:
- 通过前端应用程序的配置文件(
appsettings
、app.config 等) - 直接在上下文的构造函数中
- 通过重写
DbContext
的OnConfiguring
方法
选择哪种方式并不重要,但我正在使用选项 3。如果你使用配置设置或依赖注入,我建议使用构造函数。
重写 OnConfiguring
并不难,但让 EF 提供连接字符串可能会有点棘手。
EF 不仅用于 MSSQL,也用于其他数据库结构,如 Oracle 和 MySQL。我们需要通过安装正确的包来告诉 EF 我们想要使用哪种数据库类型。在这种情况下,我们要安装 Microsoft.EntityFrameworkCore.SqlServer 包。它包含了连接到 MSSQL 数据库、读写等所有功能。
重写 OnConfiguring
并添加代码如下所示:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;
Initial Catalog=efdordummies;Integrated Security=True;Connect Timeout=30;
Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;
MultiSubnetFailover=False");
}
DbContextOptionsBuilder
直接与 EF 和 EF 的设置相关联。通过安装 Microsoft.EntityFrameworkCore.SqlServer
包,我们可以告诉这个选项构建器我们想要使用给定的连接字符串连接到 (MS)SQL 服务器。
题外话!
不要将连接字符串硬编码到上下文中。最好将其存储在配置文件中。如果你有不同的环境(开发、测试、生产),你不会想在每次迁移到不同环境时都更改连接字符串。使用配置文件可以让你为每个环境创建设置,而无需再次更改它们。
基础已经搭建并准备就绪。接下来是与实体的关联。
DbSet
为了告诉 Entity Framework 我们的实体是什么以及如何将它们存储在数据库中,我们使用 DbSet
。它是一个 Entity Framework 属性,可以获取实体类型和 - 在本例中 - 表的名称。我们想在数据库中存储电影信息,并且为此有一个模型:Movie
。我们可以(重)用它作为我们的实体。它看起来像这样:
internal class MovieContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;
Initial Catalog=efdordummies;Integrated Security=True;Connect Timeout=30;
Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;
MultiSubnetFailover=False");
}
}
我声明了一个新的属性 Movies
。这也将成为数据库中的表名。Movies
的类型是 DbSet<Movie>
,这意味着我想将 Movie
对象作为实体附加到上下文中。这些信息将在本教程的后续部分中介绍如何在数据库中使用。
Migrations
拥有一个配置了数据库连接的上下文类和一个实体固然好,但我们仍然需要创建实际的数据库,包括表(Movies
)。以前,我们必须手动创建数据库,使用 SQL Management Studio 等工具。但 EF 可以为我们完成。由于我们使用的是 Code-First 结构,我们可以创建一个脚本来更新数据库中我们对实体所做的更改。我们将这些脚本称为迁移(migrations)。
你使用命令创建迁移。你可以使用程序包管理器控制台、Developer PowerShell 或命令提示符来执行此命令。要创建迁移,只需使用以下命令:
add-migration Initial
第一部分 add-migration
将创建一个名为第二部分(initial
)的迁移。然而,当你执行此操作时,你会遇到一个错误:
add-migration : The term 'add-migration' is not recognized as the name of a cmdlet,
function, script file, or operable program.
Check the spelling of the name, or if a path was included,
verify that the path is correct and try again.
At line:1 char:1
+ add-migration Initial
+ ~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (add-migration:String) [],
CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
不用惊慌!我们只需要安装另一个包。我是否提到过 EF 主要就是关于安装包的?这次,我们需要安装 Microsoft.EntityFrameworkCore.Tools。如果你查看说明,你会注意到它会安装几个命令行命令:
安装此包后,我们可以再次执行 add-migration
。此时会发生几件事,按顺序进行:
- Visual Studio 将生成解决方案,检查是否存在错误。
- EF 将检查数据库是否有一个快照(稍后讨论)。
- EF 将创建一个迁移文件,其中包含将用于更新数据库的 C# 代码。
如果你仔细观察,你会注意到我没有说数据库会被实际更新。这个 add-migration
只是一个创建更改的工具,而不是执行更改的工具。
但是 EF 是如何知道更改是什么的呢?通过使用一个快照。add-migration
完成后,你的项目中会创建一个新文件夹。Migrations 文件夹将保存所有你创建的迁移。这些文件也可以推送到 GIT 仓库。你会看到两个文件:
- 一个带有
时间戳
和迁移名称(在本例中为 initial)的文件。 - 一个快照文件(在本例中为 MovieContextModelSnapshot.cs)。
如果你打开快照文件,你会看到 movies
表的表示以及该表的属性。你使用的实体越多,这个文件就会越大。最终,快照是基于你的实体生成的完整数据库,而不是服务器上的数据库。
另一个文件,即迁移本身,只包含你对实体或结构所做的更改的表示。这个文件只包含 Movies
的创建。它看起来与快照不同。
namespace EntityFrameworkForDummies.ConsoleApp.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Movies",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>
(type: "nvarchar(max)", nullable: false),
Rating = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Movies", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Movies");
}
}
}
有两个方法:Up
和 Down
。Up
方法用于更新数据库。此方法的内容将被转换为 SQL 脚本并在 SQL 服务器上执行。你可以看到 migrationBuilder
将创建一个名为(CreateTable
)的表,其名称为(Movies
)以及列(Id
、Title
、Rating
)。在这里,你还可以看到这些列的类型。EF 已将 C# 的 string
翻译成 nvarchar(max)
。
如果你想撤销迁移,将执行 Down
方法。在这种情况下,很简单:只需删除 Movies
表,迁移就完成了。
建议不要更改这些方法。仅仅因为它们是自动生成的,并且代表你的实体 Movie
。想改变什么?最好创建一个新的迁移。
这很酷,但我们仍然没有数据库。正确!但你只需要执行以下命令:
update-database
此命令将检查哪些迁移尚未执行。*使用快照?*不,而是使用 __EFMigrationsHistory
表。但第一次时,此表不存在。这是 EF 创建数据库、创建 __EFMigrationsHistory
并按创建日期顺序执行所有迁移的提示。
这个 __EFMigrationsHistory
记录了哪些迁移已在数据库上执行。如果你查看此表中的数据,你会看到只有初始迁移。如果你创建了一个新的迁移,initial 迁移将不会再次执行,只会执行新的迁移。如果你删除所有迁移历史记录(别这么做!),然后再次更新数据库,它将再次执行所有迁移,导致错误,因为 Movies
表已存在。
除了历史信息,你还可以看到 Movies
表。属性(列)与我们在实体中使用的属性相同。
迁移的另一个优点是你可以与团队中的其他开发人员进行交换。我们大多数人使用 Git(hub) 来存储代码,其他开发人员也可以获取该代码。迁移只是可以推送到 git 的文件,让其他开发人员获取这些文件,并使用你创建的迁移更新他们的数据库。这样,你们所有人都有相同的数据库结构。
进行更改
我们已经有了数据库和 Entity Framework。现在我们只需要更改服务的代码。我们需要在 MovieService
类中做几件事:
- 初始化上下文类。
- 重构方法。
- 删除硬编码电影列表。
我们将保持 Program.cs 不变,因为它依赖于 MovieService
的信息。
使用数据上下文
也许这是最简单的一步。让我们创建一个 private
属性来保存一个已初始化的 MovieContext
。然后我们创建一个构造函数来设置这个属性:
private MovieContext _movieContext { get; set; }
public MovieService()
{
_movieContext = new MovieContext();
}
现在我们可以在整个类中使用 _movieContext
,其中包含已初始化的 MovieContext
。
重构方法
这一步有点棘手。我们需要让当前方法使用 MovieContext
。一些方法很容易更改,比如 GetAll()
和 Get(int id)
。先处理 GetAll()
:
public IEnumerable<Movie> GetAll()
{
return _movieContext.Movies.OrderBy(x => x.Title);
}
public Movie? Get(int id)
{
return _movieContext.Movies?.SingleOrDefault(x => x.Id == id);
}
我将 _movies
替换为 _movieContext.Movies
。就这样!EF 在运行时调用此方法时,会将此代码更改为 TSQL 查询,看起来像这样:SELECT Id
, Title
, Rating FROM Movies
。
第二个方法 Get(int id)
进行了相同的更改:将 _movies
替换为 _movieContext
。EF 将创建一个类似这样的查询:Select Id, Title, Rating FROM Movies WHERE Id = #Id#
。
接下来,让我们修复 Insert
方法。在“旧”方法中,我必须自己找出 ID。但 EF 会设置 Id
,因为它是一个键并且是自动递增的。我们可以删除这行代码。然后,我们可以将 _movies
替换为 _moviesContext.Movies
。都搞定了?不。我们需要告诉 EF 提交更改。
通过 EF 删除、更新和创建数据库中的数据并不是开箱即用的。我们需要使用 SaveChanges()
将更改保存到数据库。这个方法存在于上下文类中,该类继承自 DbContext
。
SaveChanges()
将创建一个事务,并执行上下文中(在我们的例子中是 MoviesContext
)所有当前未执行的更新、创建和删除。你可以创建更多要保存到数据库的电影,并在完成后立即执行 SaveChanges
。
SaveChanges()
返回一个 int
,但这不是实体的 **Id**。它是事务成功执行后受影响的行数。
如果我们将所有内容放在一起,我们的 Insert
方法将如下所示:
public void Insert(Movie movie)
{
if (string.IsNullOrEmpty(movie.Title))
throw new Exception("Title is required.");
_movieContext.Movies.Add(movie);
_movieContext.SaveChanges();
}
Delete(int id)
函数现在并不难。我们需要通过调用 Get(int id)
(检查)从数据库检索实体,然后从数据库中删除该实体。
还记得我说过上下文会跟踪实体吗?要从数据库检索实体,EF 添加了跟踪。然后我们可以从数据库中删除实体。别忘了调用 SaveChanges()
!
public void Delete(int id)
{
Movie? toDelete = Get(id);
if (toDelete != null)
{
_movieContext.Movies.Remove(toDelete);
_movieContext.SaveChanges();
}
}
更改实体属性
每个人都会犯错误,这没关系。错误就是用来纠正的。在这种情况下,我完全忘记为电影添加剧情(plot),这也很重要。剧情讲述了电影的一些内容,如果你想决定电影是否值得观看,这一点非常重要。
让我们在 Movie
对象中添加 Plot
。如果你好奇的话,它是一个 string
。
internal class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public int Rating { get; set; }
public string Plit { get; set; }
}
现在我们需要将 Plot
添加到数据库的 Movies
表中。我们只需创建一个新的迁移并更新数据库:
> add-migration AddingPlot
> update-database
结果
哎呀……我打错了字(...)。我创建了 Plit
而不是 Plot
。正如我所说:每个人都会犯错误,这没关系。让我们来纠正这个错误。
我有两个选择:
- 我可以将
Plit
重命名为Plot
,创建一个新的迁移,然后更新数据库。 - 我可以回滚之前的迁移,重命名
Plit
为Plot
,创建一个新的迁移,然后更新数据库。
我认为大多数人会选择选项 1。缺点是你有两个基本上相同的迁移,当更新数据库时,它们都会被执行。如果你犯更多的错误,迁移的数量会迅速增加。
更明智的做法是选择选项 2。回滚迁移,删除它,然后重试。为此,你首先需要将数据库更新到最新的正确迁移。在我们的示例中,就是初始迁移:
update-database Initial
这将导致以下日志:
PM> update-database Initial
Build started...
Build succeeded.
Reverting migration '20221130023924_AddingPlot'.
Done.
“Reverting migration”(撤销迁移)… 有趣。Entity Framework 现在已经执行了 Plot
迁移的 Down
方法。如果你查看数据库,会发现 Plit
列已消失。
现在剩下要做的就是修复 Movie
对象中的拼写错误,创建一个新的迁移,然后像上一章那样更新数据库。这样我们就能获得干净的迁移,而不是在修复上再做修复。
结论
Entity Framework 可能是用 C# 创建需要某种数据库的应用程序时最重要的部分。它可以帮助我们(开发者)摆脱对数据库结构和 SQL Studio Management 等应用程序的依赖。
实现 Entity Framework 并不难,将现有应用程序重构为使用 Entity Framework 也是小菜一碟。即使应用程序已经有一个可用的数据库,只需使用 Database-First 方法;让 Entity Framework 将数据库导入到解决方案中。但从头开始使用 Code-First 方法创建数据库更容易,因为你可以更好地控制代码和数据库中发生的事情。
迁移的使用(尤其是迁移文件)是管理数据库的好方法。你可以轻松地撤销错误,并通过 Git 或其他存储库系统在团队成员之间共享文件。这样,你就能始终拥有一个干净且一致的数据库。
本文仅涵盖了基础知识。但 Entity Framework 还有很多内容。我希望你能从中获得一些入门的思路。
历史
- 2022 年 12 月 2 日:初始版本