目录
介绍
开发人员,包括您的开发人员,一直在努力映射来自数据源和代码对象的数据。但随后,引入了ORM。ORM代表object-relational mapping,使我们能够使用领域对象和属性。测绘斗争结束了。我记得在我的代码中使用了Automapper和nHibernate。但随后,微软在2008年发布了实体框架。
像往常一样,新发布的框架——建立在ADO.NET之上——没有被广泛接受。它有很多错误,没有做预期的事情。2010年,微软发布了新版本,效果更好一些。他们一直在它的基础上构建,现在它是使用Microsoft编程语言(C#,VB等)时最常见的ORM之一。当前版本是实体框架核心7.0。
正如我之前所说,实体框架基于ADO.NET构建,但它还有其他选项。例如,映射是一个非常有用的功能。许多方面(尤其是错误)可能与ADO.NET有关。最好稍微了解一下ADO,以便了解实体框架的幕后内容。
当您使用实体框架时,实体是一件大事。不是因为它以框架的名称命名,而是它是如何工作的。实体通常是代码中的对象,表示数据源中的实体。我想说它代表数据库中的一个表。实体的属性在数据库中用作列和设置。例如:如果您有一个电影实体,它有一个Id (int)、Title (string),可能还有一个Rating (int)。实体框架可以将这些内容转换为MSSQL表,其中包含Id(int)、Title (varchar(500))和Rating (int)列。
但实体框架还可以将数据从数据库表映射回代码中的对象(实体),反之亦然。我将在本教程中向您展示这是如何工作的实体框架。
我将在本博客的其余部分使用代表实体框架的缩写EF。
代码优先与数据库优先
EF有两种管理数据库的方法。在本教程中,我将只解释其中之一;代码优先。另一个首先是数据库。它们之间有很大的区别,但代码优先是最常用的。但在我们深入研究之前,我想解释一下这两种方法。
当已经存在数据库并且数据库不会由代码管理时,将使用数据库优先。当没有当前数据库,但您希望创建一个数据库时,将使用代码优先。
我更喜欢代码,因为我可以编写实体(这些基本上是具有属性的类)并让EF相应地更新数据库。这只是C#,我不必担心数据库。我可以创建一个类,告诉EF它是一个实体,更新数据库,一切都完成了!
数据库优先是相反的方式。你让数据库“决定”你得到什么样的实体。首先创建数据库,然后相应地创建代码。
如前所述,本教程是关于代码优先的。
在我们开始之前...
在开始使用实体框架之前,我想从没有实体框架或数据库的基本控制台应用程序开始。我创建了一个可以从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变量_moviespublic。这是所有电影的列表。这些电影是硬编码的,你不会在任何数据源中找到它们。在本教程结束时,这些影片位于MSSQL数据库中,这些方法将数据读取和写入同一数据库。
让我们开始吧!
上下文
使用实体框架,一切都从上下文开始。它将实体和关系与实际数据库相关联。实体框架附带DbContext,这是我们将在代码中使用的上下文。使用DbContext,您可以在数据库中读取和写入数据,跟踪对对象所做的更改等等。我不会涵盖所有主题,只是基础知识。
数据库上下文类
我们需要做的第一件事是创建一个上下文类。这只是一个普通的C#类,继承自DbContext。DbContext是实体框架的一个类。您需要先安装一个软件包,然后才能使用它: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
数据源是服务器的位置。在我的例子中,它是(localdb)\MSSQLLocalDb,它代表Visual Studio附带的本地数据库。
初始目录是数据库的名称。给它一个好的、有意义的名字。使用EF,无需自行创建数据库;如果需要,EF将为你执行此操作。
这是目前最重要的两个设置。我不会详细介绍其他选项。
EF需要了解连接string,我们可以通过多种方式将其提供给EF:
- 通过前端应用程序的配置文件(appsettings、app.config 等)
- 直接在上下文构造函数中
- 通过OnConfiguring重写DbContext
你选择哪一个并不重要,但我正在使用选项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
为了告诉实体框架我们的实体是什么以及如何将它们存储在数据库中,我们使用DbSet。它是一个实体框架属性,它将获取实体的类型和表名称(在本例中为表名称)。我们希望将电影信息存储在我们的数据库中,我们有一个模型: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作为实体附加到上下文中。如何在数据库中使用此信息是本教程后面的主题。
迁移
拥有一个与数据库和实体的已配置连接的上下文类很好,但我们仍然需要创建实际的数据库,包括表(Movies)。过去,我们必须使用SQL Management Studio等工具手动创建数据库。但EF有能力为我们做到这一点。由于我们使用的是代码优先构造,因此我们可以创建一个脚本,该脚本将使用我们对实体所做的更改来更新我们的数据库。我们将这些脚本称为迁移。
您可以使用命令创建迁移。可以使用包管理控制台、开发人员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如何知道更改是什么?通过使用快照。添加迁移完成后,已在项目中创建了一个新文件夹。“迁移”文件夹将保存您创建的所有迁移。这些文件也可以推送到GIT存储库。您将看到两个文件:
- 一个带有timestamp和迁移名称的文件,在我们的例子中是首字母。
- 在我们的例子中是一个快照文件,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将创建一个具有名称(Movies)和列(Id,Title,Rating)的表(CreateTable)。在这里,您还可以看到这些列的类型。EF已将C# string转换为nvarchar(max)。
如果要反向迁移,将执行Down-方法。在这个例子中,很简单:只需删除Movies表,迁移就会撤消。
建议不要更改这些方法。仅仅因为它们是自动生成的并代表您的Movie实体。想要改变一些东西吗?最好创建新的迁移。
这很酷,但我们仍然没有数据库。正确!但是您唯一需要做的就是执行以下命令:
update-database
此命令将检查哪些迁移尚未执行。通过使用快照?不,通过使用__EFMigrationsHistory表。但此表不是第一次可用。这是EF创建数据库、创建__EFMigrationsHistory并按创建日期顺序执行所有迁移的提示。
__EFMigrationsHistory将跟踪在数据库上执行了哪些迁移。如果查看此表中的数据,则只会看到初始迁移。如果要创建新迁移,则不会再次执行初始迁移,而只会执行新迁移。如果您删除迁移历史记录的所有记录(不要!)并再次更新数据库,它将再次执行所有迁移,从而导致错误,因为Movies表已存在。
除了历史记录信息外,您还可以看到Movies -表。属性(列)类似于我们在实体中使用的属性(列)。
迁移的另一个重要功能是您可以与团队中的其他开发人员交换它们。我们大多数人使用Git(hub)来存储代码,其他开发人员也可以获取该代码。迁移只是您可以推送到git的文件,让其他开发人员获取这些文件,并使用您创建的迁移更新他们的数据库。这样,你们都有相同的数据库结构。
进行更改
我们有我们的数据库和实体框架。我们现在要做的就是更改服务的代码。我们需要在MovieService类上做几件事:
- 初始化上下文类。
- 重构方法。
- 删除硬编码电影列表。
我们按原样保留program.cs因为它依赖于来自MovieService。
使用数据上下文
也许这是最简单的步骤。让我们创建一个包含MovieContext初始化的private属性。然后我们创建一个构造函数来设置此属性:
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);
}
我使用_movieContext.Movies替换_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)(check)并从数据库中删除实体来从数据库中检索实体。
还记得我说过上下文跟踪实体吗?为了从数据库中检索实体,EF添加了跟踪。然后,我们可以从数据库中删除该实体。别忘了调用SaveChanges()!
public void Delete(int id)
{
Movie? toDelete = Get(id);
if (toDelete != null)
{
_movieContext.Movies.Remove(toDelete);
_movieContext.SaveChanges();
}
}
更改实体属性
每个人都会犯错,这没关系。错误在这里需要修复。在这种情况下,我完全忘记了在电影中添加情节,这也很重要。情节讲述了一些关于电影的信息,如果您想决定电影是否适合您观看,这一点非常重要。
让我们将情节添加到Movie对象中。这是一个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.
“恢复迁移”...有趣。实体框架现已执行Plot迁移的Down方法。如果您查看数据库,您会注意到该Plit列消失了。
现在剩下的就是修复Movie对象中的拼写错误,创建新的迁移,并更新数据库,如上一章所示。这为我们提供了干净的迁移,而不是修复修复。
结论
在C#中创建需要某种形式的数据库的应用程序时,实体框架可能是最重要的部分。它可以帮助我们(开发人员)远离数据库结构和应用程序,如SQL Studio Management。
实现实体框架并不难,重构现有应用程序以使用实体框架是小菜一碟。即使应用程序具有工作数据库,也只需使用数据库优先方法;让我们实体框架将数据库导入解决方案。但是,使用代码优先方法从头开始创建数据库更容易,因为您可以更好地控制代码和数据库中发生的情况。
使用迁移(尤其是迁移文件)是管理数据库的好方法。您可以轻松地逆转错误,并通过Git或其他存储库系统在团队中共享文件。这样,您就可以始终拥有一个干净且一致的数据库。
本文仅介绍基础知识。但实体框架还有更多内容。我希望你对如何开始有所了解。
https://www.codeproject.com/Articles/5348568/Entity-Framework-Fundamentals