使用 Entity Framework 的数据库优先





5.00/5 (10投票s)
通过数据库优先方法学习 Entity Framework 的基础知识
引言
如果您使用代码优先方法,您没有数据库,而是通过迁移来创建数据库。您可以创建新的实体并“推送”到数据库。如果数据库尚不存在,Entity Framework 会创建它,并用实体中的更改更新数据库。
代码优先的优点在于,您可以使用 C# 控制整个数据库流程和设计。您不受遗留代码的限制。大多数设置,如表的字段,都可以通过代码设置,而且您可以保持简单。
在某些情况下,您没有代码优先的奢侈,而是被现有数据库所困扰。删除数据库然后用代码优先方法重建它不是一个好主意。原因很简单:删除数据库也会删除数据。迁移数据可能会很麻烦,而且由于可能存在您未注意到的配置,如索引或触发器,您可能会遇到错误。
在这种情况下,您希望 Entity Framework 读取现有数据库并生成实体。这样,您就可以创建代表实体的 C# 类。如果正确完成,您可以继续在 C# 中添加和更改实体,并使用迁移来更新数据库。
开始之前
在大多数文章中,我都有一个简单的控制台应用程序,其中包含一些代码可供参考。通常是关于一些电影。在这种情况下,我没有控制台应用程序,但我有一个数据库。表和列看起来是这样的
如果您想跟随本文的步伐,这里是您可以执行的 SQL 来创建上述数据库
CREATE Database Movies
GO
USE Movies2
CREATE TABLE [dbo].[Movies]
(
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
[Title] VARCHAR(100) NULL,
[Rating] NCHAR(10) NULL
)
CREATE TABLE [dbo].[Actors]
(
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
[Name] VARCHAR(500) NULL,
[Age] INT NULL,
[Gender] INT NULL
)
CREATE TABLE [dbo].[ActorsMovies]
(
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
[ActorId] INT NULL,
[MovieId] INT NULL
)
ALTER TABLE ActorsMovies
ADD CONSTRAINT FK_ActorId_Actors_Id FOREIGN KEY (ActorId)
REFERENCES Actors (Id)
ON DELETE CASCADE
ON UPDATE CASCADE
;
ALTER TABLE ActorsMovies
ADD CONSTRAINT FK_MovieId_Movies_Id FOREIGN KEY (MovieId)
REFERENCES Movies (Id)
ON DELETE CASCADE
ON UPDATE CASCADE
;
DECLARE @actorId int
DECLARE @movieId int
INSERT INTO Movies (Title, Rating) VALUES ('Shrek', 5)
SET @movieId = @@IDENTITY
INSERT INTO Actors (Name, Age, Gender) VALUES ('Cameron Diaz', 50, 1)
SET @actorId = @@IDENTITY
INSERT INTO ActorsMovies (ActorId, MovieId) VALUES (@actorId, @movieId);
INSERT INTO Actors (Name, Age, Gender) VALUES ('Mike Myers', 59, 0)
SET @actorId = @@IDENTITY
INSERT INTO ActorsMovies (ActorId, MovieId) VALUES (@actorId, @movieId);
INSERT INTO Movies (Title, Rating) VALUES ('Inceptiopn', 1)
SET @movieId = @@IDENTITY
INSERT INTO Actors (Name, Age, Gender) VALUES ('Leonardo DiCaprio', 48, 0)
SET @actorId = @@IDENTITY
INSERT INTO ActorsMovies (ActorId, MovieId) VALUES (@actorId, @movieId);
INSERT INTO Movies (Title, Rating) VALUES ('The Matrix', 4)
SET @movieId = @@IDENTITY
INSERT INTO Actors (Name, Age, Gender) VALUES ('Keanu Reeves', 58, 0)
SET @actorId = @@IDENTITY
INSERT INTO ActorsMovies (ActorId, MovieId) VALUES (@actorId, @movieId);
INSERT INTO Movies (Title, Rating) VALUES ('The Muppets', 5)
SET @movieId = @@IDENTITY
INSERT INTO Actors (Name, Age, Gender) VALUES ('Kermit the Frog', 67, 0)
SET @actorId = @@IDENTITY
INSERT INTO ActorsMovies (ActorId, MovieId) VALUES (@actorId, @movieId);
此数据库将充当我们需要用 Entity Framework `import` 的数据库。
我确实创建了一个控制台应用程序,但不需要任何代码。Entity Framework 会提供代码。我将把这个应用程序命名为 `Movies.ConsoleApp`,并使用 .NET 7。
Scaffold-DbContext
现在我们的数据库已经准备好了。是时候从数据库获取信息并将其放入代码中了。是的,数据库优先意味着我们从现有数据库创建 C# 代码。
要做到这一点,我们需要一些东西
- 命令行
我将使用 Visual Studio 2022 中的程序包管理器控制台。 - 到数据库的连接字符串
- 一个空项目
我已经有了空的控制台应用程序。 - NuGet 程序包 `Microsoft.EntityFrameworkCore.Tools`
这应该安装在您的项目中。
一切就绪后,就可以从数据库获取信息并创建 C# 代码了。这个过程我们称之为*脚手架*。
要对数据库进行脚手架,您需要使用 `Scaffold-DbContext` 命令。它有几个参数,例如到数据库的连接字符串、提供程序(您正在使用哪种数据库类型)以及 Entity Framework 要将 C# 文件放置到的输出目录。还有其他参数,但目前我们不需要它们。
命令看起来会有点像这样
Scaffold-DbContext [-Connection] [-Provider] [-OutputDir]
要查找 `Connection` 字符串,无需费心。只需在*SQL Server 对象资源管理器*中打开数据库,展开数据库,右键单击名称,选择**属性**,然后找到**连接字符串**属性。只需复制该值并将其粘贴到命令的 `[-Connection]` 参数中。
`Provider` 是数据库的类型。我使用 SQL Server,所以提供程序将是 `Microsoft.EntityFrameworkCore.SqlServer`。另一个提供程序的例子是 Oracle,它将使用 `Oracle.EntityFrameworkCore` 命名空间作为提供程序。不要忘记使用 NuGet 安装正确的提供程序程序包!由于我将使用 SQL Server,因此我需要安装 `Microsoft.EntityFrameworkCore.SqlServer`* 包。
最后一个参数 `OutputDir` 是文件夹,如果您愿意,也可以是目录,Entity Framework 将在那里放置文件。在我的例子中,我将使用*Entities*。
将所有这些放在一起,您可能会得到类似这样的东西
Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;
Initial Catalog=Movies;Integrated Security=True;Connect Timeout=30;
Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;
Multi Subnet Failover=False" Microsoft.EntityFrameworkCore.SqlServer
-OutputDir "Entities"
如果您运行此命令,Entity Framework 将开始从数据库检索信息,并在*Entities*文件夹中创建文件。
生成的文件
正如您可能已经自己发现的那样,有几个文件看起来很熟悉。`Actor`、`ActorsMovie` 和 `Movie`* 文件是实体,代表数据库中的表。`Movies2Context.cs` 文件是上下文文件,其中包含一个继承自 `DbContext` 的类。
那么,您可能自己用代码优先方法做的事情,Entity Framework 已经为您完成了。
如果我们打开一个实体,它看起来并不特别。让我们看一下 `Actor` 实体。
public partial class Actor
{
public int Id { get; set; }
public string? Name { get; set; }
public int? Age { get; set; }
public int? Gender { get; set; }
public virtual ICollection<ActorsMovie> ActorsMovies { get; } = new List<ActorsMovie>();
}
它拥有我们期望的所有属性。除了…… `Name` 在数据库中的长度为 500。我们通常为此使用一个属性。那么,这被忽略了吗?不,它没有。我稍后会回来谈谈。
最后一个属性 `ActorsMovies` 是因为 `ActorsMovies` 和 `Actor` 之间的关系而创建的,正如您在前面给出的 SQL 脚本中所看到的。此属性也将存在于 `Movie` 实体中。
我认为这很容易理解。让我们看一下上下文类,`Movies2Context`。
public partial class Movies2Context : DbContext
{
public Movies2Context()
{
}
public Movies2Context(DbContextOptions<Movies2Context> options)
: base(options)
{
}
public virtual DbSet<Actor> Actors { get; set; }
public virtual DbSet<ActorsMovie> ActorsMovies { get; set; }
public virtual DbSet<Movie> Movies { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;
Initial Catalog=Movies2;Integrated Security=True;
Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;
Application Intent=ReadWrite;Multi Subnet Failover=False");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Actor>(entity =>
{
entity.HasKey(e => e.Id).HasName("PK__Actors__3214EC078BB3A95D");
entity.Property(e => e.Name)
.HasMaxLength(500)
.IsUnicode(false);
});
modelBuilder.Entity<ActorsMovie>(entity =>
{
entity.HasKey(e => e.Id).HasName("PK__ActorsMo__3214EC072E0DD618");
entity.HasOne(d => d.Actor).WithMany(p => p.ActorsMovies)
.HasForeignKey(d => d.ActorId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_ActorId_Actors_Id");
entity.HasOne(d => d.Movie).WithMany(p => p.ActorsMovies)
.HasForeignKey(d => d.MovieId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_MovieId_Movies_Id");
});
modelBuilder.Entity<Movie>(entity =>
{
entity.HasKey(e => e.Id).HasName("PK__Movies__3214EC07E0F7DED3");
entity.Property(e => e.Rating)
.HasMaxLength(10)
.IsFixedLength();
entity.Property(e => e.Title)
.HasMaxLength(100)
.IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
“我的天……这里发生了什么??”是我第一次使用数据库优先方法时的第一反应。让我们一步一步地看。首先,它有一个空的构造函数。不知道为什么。然后有一个带有 `DbContextOptions` 参数的构造函数,这听起来更有意义,因为我们需要设置连接字符串。
然后我们看到 `DbSets`,它连接了实体和表名。这里没什么特别的。
接下来是 `OnConfiguring` 的重写。它实际上将连接硬编码了。这是我们绝对不想要的。如果我们有多个环境,您需要一个通用的连接字符串。也许在 `appsettings` 或其他地方。
然后我们有了 `OnModelCreating` 的重写。好吧,感觉是个好主意。这里它设置了主键以及…… tadaaa……字段的最大长度。Entity Framework 在这里完成了大部分关于实体和表的配置。也许不是理想的,但它有效。
进行一些(数据库)更改
好的,一切都准备好了,我们有了 C# 代码。多么棒啊!但是,我们如何对数据库进行更改呢?比如,我们想在 `Movies` 表中添加一个字段。或者我们想添加一个全新的表!使用数据库优先,它的工作方式略有不同。
使用代码优先方法,我们可以根据对实体和 `DataContext` 所做的更改创建迁移。之后,我们可以简单地发送 `Update-Database` 命令,SQL(或 Oracle 或……任何其他)就会被更新。
这不适用于数据库优先,因为实际数据库是主导。在这种情况下,我们需要对真实数据库进行更改,然后从数据库获取更改,并将它们放入我们的 C# 代码中。
如果您像我第一次那样想:“不,我 just 创建一个迁移并更新我的数据库。”当然,尽管去吧,但迁移将包含数据库中已有的所有现有表和字段;以前从未有过迁移。
好吧,举个例子。让我们向 `Movie` 实体添加 `ReleaseDate` 字段。这是一个 `DateTime`。第一步是在 SQL 数据库的表中添加它。
ALTER TABLE [dbo].[Movies]
ADD [ReleaseDate] DATETIME NULL;
您可以在自己的 SQL IDE 或 Visual Studio 中执行此 SQL 语句。
控制台应用程序仍然可以正常工作,因为它不知道 `Movies` 表的更改。要让它知晓这一点,我们需要重新进行脚手架。但是如果您运行我们之前使用的命令,您会收到一个错误
以下文件已存在于目录 C:\YOUR_PATH_HERE\Movies.ConsoleApp\Entities 中:Actor.cs,ActorsMovie.cs,Movie.cs。请使用 Force 标志覆盖这些文件。
它建议使用 `Force` 标志,这意味着它将覆盖所有实体和 `DataContext`。这是个好主意吗?嗯,也许吧。无论如何,您都不应该更改脚手架代码。但是如果您有一个大型数据库,可能需要一些时间。尤其是在数据库不是本地的情况下。
有一种方法可以告诉 Entity Framework 您想对哪些表进行脚手架。请看下面的命令
Scaffold-DbContext
"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Movies;
Integrated Security=True;Connect Timeout=30;Encrypt=False;
Trust Server Certificate=False;Application Intent=ReadWrite;
Multi Subnet Failover=False"
Microsoft.EntityFrameworkCore.SqlServer
-OutputDir "Entities"
-Tables Movies
-Force
我添加了 `-Tables` 标志,后面跟着我想进行脚手架的表的名称。但我们仍然需要 `force` 标志,否则它仍然会抱怨文件 *Movies.cs* 已存在。执行此命令后,让我们看一下 *Movies.cs*。
public partial class Movie
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Rating { get; set; }
public DateTime? ReleaseDate { get; set; }
}
那里有 `ReleaseDate`。这并不神奇,但相当酷。
在其他项目中进行脚手架
如果您想在另一个项目(例如数据层)中对数据库信息进行脚手架,您可以使用与之前相同的命令,但更改程序包管理器控制台中的默认项目。确保将包含 `Microsoft.EntityFrameworkCore.Tools` 的项目设为启动项目。应该获得脚手架数据库的项目需要被引用到启动项目中。
结论
说实话:我真的不喜欢数据库优先的方法。仅仅因为我一直需要一个真实的数据库。如果您不小心,它还会弄乱您的项目。
但是……在某些情况下,您需要它。如果您正在处理一个遗留系统(也称为旧系统),您可能有一个包含数据和所有配置的数据库。如果您想使用任何形式的 Entity Framework,您就被限于数据库优先的方法。除非您能说服您的团队和/或老板用代码优先的方法重建数据库。
数据库优先方法的好处之一是,`DataContext` 中的大量配置是由 Entity Framework 创建的,而您不必弄清楚如何设置所有实体之间的关系。这是我仍然无法正确做到的事情。
历史
- 2023 年 2 月 15 日:初始版本