65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Entity Framework 的数据库优先

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2023 年 2 月 15 日

CPOL

8分钟阅读

viewsIcon

34692

通过数据库优先方法学习 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 日:初始版本
© . All rights reserved.