学习 Entity Framework(第 2 天):Microsoft .NET Entity Framework 中的 Code First 方法和迁移






4.98/5 (14投票s)
Microsoft .NET Entity Framework 中的 Code First 方法和迁移
目录
引言
本文旨在解释 Microsoft Entity Framework 提供的 Code First 方法和 Code First 迁移。在我**上一篇文章**中,我解释了 Entity Framework 的理论以及其他两种方法,即 Database First 和 Model First 方法。我们将一步步探索 Code First 方法,通过这种方法,我们可以在应用程序中使用 Entity Framework 访问数据库和数据。我将使用 Entity Framework 版本 6.2、.NET Framework 4.6 和 Visual Studio 2017 进行本教程。对于数据库,我们将使用 SQL Server。如果没有安装 SQL Server,您可以使用 localDB。
系列信息
我们将遵循一个五篇文章的系列来详细学习 Entity Framework 的主题。所有文章都将是教程形式,最后一篇除外,我将在其中介绍 Entity Framework 的理论、历史和用法。该系列的议题如下。
- 学习 Entity Framework(第 1 天):.NET 中 Entity Framework 的数据访问方法
- 学习 Entity Framework(第 2 天):.NET 中 Entity Framework 的 Code First 迁移
- 学习 Entity Framework(第 3 天):.NET 中 ASP.NET WebAPI 2.0 的 Code First 迁移
- 学习 Entity Framework(
第 4 天): 理解 Entity Framework Core 和 EF Core 中的 Code First Migrations - 学习 Entity Framework(第 5 天):Entity Framework(理论篇)
代码优先方法
Code First 方法是 EF 的推荐方法,尤其是在从头开始开发应用程序时。您可以提前定义 POCO 类及其关系,并通过仅在代码中定义结构来设想您的数据库结构和数据模型可能的样子。Entity Framework 最后将负责为您生成的 POCO 类和数据模型生成数据库,并处理事务、历史记录和迁移。
通过所有三种方法,您可以完全控制在任何时候根据需要更新数据库和代码。
使用 Code First 方法,开发人员的重点仅在于代码,而不是数据库或数据模型。开发人员可以在代码本身中定义类及其映射,并且由于 Entity Framework 现在支持继承,因此定义关系更加容易。Entity Framework 会为您创建或重新创建数据库,而且不仅如此,在创建数据库时,您可以提供种子数据,即您希望在创建数据库时数据库中包含的表的主数据。使用 Code First,您可能不会有包含关系和架构的 .edmx 文件,因为它不依赖于 Entity Framework 设计器及其工具,并且由于您是创建类、关系和管理它们的人,因此对数据库的控制会更多。出现了一个新的 Code First 迁移概念,它使得 Code First 方法更容易使用和遵循,但在本文中,我将不使用迁移,而是使用创建 DB context 和 DB set 类的方法,以便您了解其内部工作原理。Code First 方法也可用于从现有数据库生成代码,因此基本上它提供了两种使用方法。
Code First 方法详解
- 创建一个名为
EF_CF
的新控制台应用程序。这将为您提供 Program.cs 文件和一个包含Main()
方法的文件。 - 现在我们将创建模型类,即 POCO(Plain Old CLR Object)类。例如,假设我们要创建一个应用程序,其中将对
employee
进行数据库操作,并且employee
将被分配到一个department
。因此,一个department
可以有多个employees
,而一个employee
只能有一个department
。因此,我们将创建前两个实体Employee
和Department
。向项目中添加一个名为Employee
的新类,并为其添加两个简单的属性,即EmployeeId
和EmployeeName
。 - 类似地,添加一个名为
Department
的新类,并添加DepartmentId
、DepartmentName
和DepartmentDescription
属性,如下所示。 - 由于一个
employee
属于一个department
,每个employee
都将有一个相关的department
,因此向Employee
类添加一个名为DepartmentId
的新属性。 - 现在是时候将
EntityFramework
添加到我们的项目中了。打开程序包管理器控制台,选择您的当前控制台应用程序作为默认项目,然后安装 Entity Framework。我们之前已经执行过几次了,所以现在安装它不会有问题。 - 由于我们是从头开始做所有事情,所以我们也需要
DbContext
类。在 Model First 和 Database First 方法中,我们已经生成了 DB context 类。但在这种情况下,我们需要手动创建它。向项目添加一个名为CodeFirstContext
的新类,该类继承自namespace System.Data.Entity
的DbContext
类,如下面的图像所示。现在添加两个名为Employees
和Departments
的DbSet
属性,如下面的图像所示。最终代码可能如下所示:
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EF_CF { public class CodeFirstContext: DbContext { public DbSet<Employee> Employees { get; set; } public DbSet<Department> Departments { get; set; } } }
DbContext
和DbSet
都是我们的超级英雄,它们创建和处理数据库操作,并为我们提供极大的便利,使我们能够高度抽象。当我们使用
DbContext
时,我们实际上是在处理实体集。DbSet
代表一个类型化的实体集,用于执行创建、读取、更新和删除操作。我们不会独立创建和使用DbSet
对象。DbSet
只能与DbContext
一起使用。 - 让我们尝试使我们的实现更抽象一些,而不是直接从控制器访问
dbContext
,让我们将其抽象到一个名为DataAccessHelper
的类中。这个类将是我们所有数据库操作的辅助类。因此,向项目添加一个名为DataAccessHelper
的新类。 - 创建 DB context 类的只读实例,并添加一些方法,例如
FetchEmployees()
来获取员工详细信息,FetchDepartments()
来获取部门详细信息。每种方法一个,用于添加employee
和添加department
。您可以根据需要添加更多方法,例如update
和delete
操作。目前,我们将仅限于这四种方法。代码可能如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EF_CF { public class DataAccessHelper { readonly CodeFirstContext _dbContext = new CodeFirstContext(); public List<Employee> FetchEmployees() { return _dbContext.Employees.ToList(); } public List<Department> FetchDepartments() { return _dbContext.Departments.ToList(); } public int AddEmployee(Employee employee) { _dbContext.Employees.Add(employee); _dbContext.SaveChanges(); return employee.EmployeeId; } public int AddDepartment(Department department) { _dbContext.Departments.Add(department); _dbContext.SaveChanges(); return department.DepartmentId; } } }
- 现在让我们添加导航属性的概念。导航属性是指类的属性,通过这些属性,可以在 Entity Framework 中通过该实体访问相关实体。因此,在获取
Employee
数据时,我们可能需要获取其相关Departments
的详细信息;在获取Department
数据时,我们可能需要获取与之关联的employees
的详细信息。Navigation
属性以virtual
属性的形式添加到实体中。因此,在Employee
类中,添加一个返回单个Department
实体的Departments
属性,并将其设为virtual
。类似地,在Department
类中,添加一个名为Employees
的属性,该属性返回Employee
实体的集合,并将其设为virtual
。以下是
Employee
和Department
模型的代码。员工
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EF_CF { public class Employee { public int EmployeeId { get; set; } public string EmployeeName { get; set; } public int DepartmentId { get; set; } public virtual Department Departments { get; set; } } }
部门
using System; using System.Collections.Generic; using System.Linq; using System.Security.Policy; using System.Text; using System.Threading.Tasks; namespace EF_CF { public class Department { public int DepartmentId { get; set; } public string DepartmentName { get; set; } public string DepartmentDescription { get; set; } public virtual ICollection<Employee> Employees { get; set; } } }
- 让我们编写一些代码来使用我们的代码执行数据库操作。因此,在 Program.cs 类的
Main()
方法中,添加以下示例测试代码。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EF_CF { class Program { static void Main(string[] args) { Department department = new Department { DepartmentName = "Technology", Employees = new List<Employee> { new Employee() {EmployeeName = "Jack"}, new Employee() {EmployeeName = "Kim"}, new Employee() {EmployeeName = "Shen"} } }; DataAccessHelper dbHelper = new DataAccessHelper(); dbHelper.AddDepartment(department); var addedDepartment = dbHelper.FetchDepartments().FirstOrDefault(); if (addedDepartment != null) { Console.WriteLine("Department Name is: " + addedDepartment.DepartmentName + Environment.NewLine); Console.WriteLine("Department Employees are: " + Environment.NewLine); foreach (var addedDepartmentEmployee in addedDepartment.Employees) { Console.WriteLine(addedDepartmentEmployee.EmployeeName + Environment.NewLine); } Console.ReadLine(); } } } }
在上面的
Main()
方法的代码中,我们试图创建一个Department
类的对象,并将Employees
列表添加到该类的Employees
属性中。创建dbHelper
类的实例,并调用AddDepartment
方法,将department entity
对象传递给该方法以添加新的department
。在添加
department
之后,我们正在获取新添加的department
,并确保department
及其相关的employees
已成功添加到数据库中。因此,我们将获取department
,并在控制台中打印department
名称及其相关的employees
。但是这一切是如何完成的呢,我们还没有数据库。☹ - 不用担心,让我们看看如何确保我们从代码创建了 DB。首先,正如我们之前所见,我们的
context
类名应与我们的连接字符串名称相同,反之亦然。因此,在 App.config 文件中添加一个与 DB context 类同名的连接字符串,如下所示。工作完成了!Entity Framework 将处理其余的数据库创建工作。我们只需运行应用程序,现在,当首次使用 DB context 类执行 DB 操作时,就会创建我们的数据库。
- 在
main
方法上设置断点并运行应用程序。 - 一旦执行到我们编写
AddDepartment
代码的那一行,我们的数据库就会被创建。 - 转到数据库服务器,查看我们创建的数据库,其名称与我们在连接
string
中提供的名称相同。我们有Departments
和Employees
表,以及一个名为__MigrationHistory
的表,用于跟踪在此数据库上执行的 Code First 迁移的历史记录。我们看到数据库中还添加了一个
Department
,其名称为“Technology
”,这是我们在代码中使用的。并且,我们的
employee
表已填充了三行,其中包含三个employees
,它们的department id
为 1,即新添加的department
的id
。因此,我们的 Code First 方法也奏效了。😊 - 您可以按 F5 继续运行应用程序,当控制台窗口出现时,我们将在该窗口中看到
department
和其中添加的employees
的详细信息,因此我们的fetch
操作也运行正常。
尽管我们已经涵盖了 Entity Framework 的所有方法,但我现在还想展示 Code First 迁移,让您了解 Code First 迁移如何与 Entity Framework 一起工作。在此之前,我们需要了解迁移的需求以及在处理 Code First 方法时拥有迁移的好处。
Code First 选项
Entity Framework Code First 方法为我们提供了三种创建数据库的方法。
CreateDatabaseIfNotExists
这是为 Code First 方法提供的 initializer
类选项。它有助于仅在没有现有数据库时创建数据库,因此可以通过此选项意外删除该数据库。
DropCreateDatabaseWhenModelChanges
这个 initializer
类会监视底层模型,如果模型发生变化,它会删除现有数据库并重新创建一个新的数据库。当应用程序未上线且仍处于开发和测试阶段时,这很有用。
DropCreateDatabaseAlways
顾名思义,此选项会在每次运行应用程序时删除并创建数据库。这在测试中非常有用,因为您每次都使用新数据集进行测试。
Code First 迁移
想象一下,您想添加一个新的模型/实体,并且不希望在用新添加的模型类更新数据库时删除或更改现有数据库。Code First 迁移在这里可以帮助您使用新添加的模型类更新现有数据库,并且您的现有数据库将保持不变,数据也保持不变。因此,数据和架构不会再次创建。
Code First 迁移详解
让我们逐步了解如何处理 Code First 迁移,就像我们为其他方法所做的那样。
- 添加一个名为
EF_CF_Migrations
的新控制台应用程序。 - 添加
Department
模型,属性包括DepartmentId
、DepartmentName
和DepartmentDescription
。添加一个virtual
属性作为navigation
属性,名为Employees
,因为一个department
可以有多个employees
。 - 类似地,添加一个名为
Employee
的模型类,并添加三个属性EmployeeId
、EmployeeName
、DepartmentId
,以及Departments
作为navigation
属性,因为一个employee
可能与任何department
相关联。 - 从程序包管理器控制台安装 Entity Framework,如下面的图像所示。
- 添加一个从
DbContext
类派生的context
类,并在类中将Employee
和Department
类添加为DbSet
属性。 - 现在,执行名为“
Enable-Migrations
”的命令,但在此之前,请将您的新添加的项目设置为默认项目。必须使用程序包管理器控制台执行该命令。 - 命令执行后,您将在应用程序中看到一个名为“Migrations”的文件夹,并且默认会添加一个名为
Configuration
的类,该类包含您的初始配置以及您希望与 Code First 方法一起使用的所有其他配置。您可以在此类的构造函数中配置设置。此类继承自DbMigrationsConfigurations
,它在基类中有一个virtual
方法Seed
。我们可以在派生类中重写该方法,以便在创建数据库时向数据库添加一些种子数据。 Seed
方法将 context 作为参数。Context
是我们的CodeFirstContext
类的实例。现在向 context 添加示例数据,例如,如下所示,我正在添加一个名为Technology
的department
,其中包含三个sample
员工,以及另外一个单独添加到 context 的employee
。类看起来与下面的代码相似。using System.Collections.Generic; namespace EF_CF_Migrations.Migrations { using System; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; internal sealed class Configuration : DbMigrationsConfiguration<EF_CF_Migrations.CodeFirstContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(EF_CF_Migrations.CodeFirstContext context) { Department department = new Department { DepartmentName = "Technology", Employees = new List<Employee> { new Employee() {EmployeeName = "Jack"}, new Employee() {EmployeeName = "Kim"}, new Employee() {EmployeeName = "Shen"} } }; Employee employee = new Employee { EmployeeName = "Akhil Mittal", DepartmentId = 1 }; context.Departments.AddOrUpdate(department); context.Employees.AddOrUpdate(employee); } } }
- 现在,在程序包管理器控制台执行另一个命令,称为“
Add-Migration Initial
”。执行此命令时,将在 Migrations 文件夹下创建另一个文件。文件名包含时间戳,并带有关键字“
_Initial
”。此类继承自DbMigration
类,该类有一个virtual Up()
方法。该命令会在生成的类中重写此方法,并添加在我们的代码执行时创建数据库表的语句。Down()
方法与Up()
方法相反。以下是我们添加初始迁移时生成的代码。
Up
方法包含数据库语句,并在创建数据库表时负责处理键约束。namespace EF_CF_Migrations.Migrations { using System; using System.Data.Entity.Migrations; public partial class Initial : DbMigration { public override void Up() { CreateTable( "dbo.Departments", c => new { DepartmentId = c.Int(nullable: false, identity: true), DepartmentName = c.String(), DepartmentDescription = c.String(), }) .PrimaryKey(t => t.DepartmentId); CreateTable( "dbo.Employees", c => new { EmployeeId = c.Int(nullable: false, identity: true), EmployeeName = c.String(), DepartmentId = c.Int(nullable: false), }) .PrimaryKey(t => t.EmployeeId) .ForeignKey("dbo.Departments", t => t.DepartmentId, cascadeDelete: true) .Index(t => t.DepartmentId); } public override void Down() { DropForeignKey("dbo.Employees", "DepartmentId", "dbo.Departments"); DropIndex("dbo.Employees", new[] { "DepartmentId" }); DropTable("dbo.Employees"); DropTable("dbo.Departments"); } } }
- 在我们继续之前,还有一个需要弥合的差距。我们需要在 App.config 中有一个与我们的
context
类同名的连接字符串。因此,打开项目的 app.config 文件,并根据需要添加连接字符串,其中包含服务器和数据库名称详细信息。 - 迁移的最后一步是执行一个名为“
Update-Database
”的命令。在程序包管理器控制台上执行此命令时,它将应用我们在 Migrations 文件夹下的所有迁移,并运行
Configuration
类的seed
方法。 - 现在,转到数据库检查是否已创建表以及其中是否包含我们在
seed
方法中提供的示例数据。在下面的图像中,我们看到Departments
表包含我们在seed
方法中作为Department
模型添加到 context 的示例department
。在
Employees
表中,我们有所有与该department
相关联的employees
,以及我们通过seed
方法添加的另一个额外的employee
。 - 让我们在 program.cs 类中添加一些代码来检查数据库操作是否正常工作。因此,创建一个
CodeFirstContext
实例,并添加另一个示例department
和示例employees
,然后保存更改。以下是代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EF_CF_Migrations { class Program { static void Main(string[] args) { CodeFirstContext context =new CodeFirstContext(); Department department = new Department { DepartmentName = "Management", Employees = new List<Employee> { new Employee() {EmployeeName = "Hui"}, new Employee() {EmployeeName = "Dui"}, new Employee() {EmployeeName = "Lui"} } }; context.Departments.Add(department); context.SaveChanges(); } } }
- 按 F5 运行代码,然后转到数据库检查
department
和与其关联的Employees
的记录是否已插入。在下面的图像中,当我们选择Departments
表中的前几条记录时,我们得到另一个我们刚刚创建的department
。- 如下面的图像所示,我们为新添加的
department
添加了Employees
。
- 如下面的图像所示,我们为新添加的
MigrationHistory 表
这是 Code First 迁移最重要的部分。我们看到,除了我们的实体表之外,还有一个名为 __MigrationHistory
的附加表。此表负责存储我们从代码添加的所有迁移历史记录。例如,请检查最初创建的那一行。第一行的 MigrationId
列包含的值与我们在代码中添加迁移时创建的文件名相同。它包含哈希值,每次我们修改模型并在运行时运行 update migrations 命令时,它都会检查数据库中的历史记录,并与我们 Migrations 文件夹中现有的迁移文件进行比较。如果文件是新的,它只会执行该文件,而不是旧文件。这有助于我们以更有条理的方式跟踪数据库更改。还可以通过提供特定迁移的迁移 ID 来从代码回滚到该迁移。迁移 ID 就是迁移文件的名称,也是存储在 __MigrationHistory
表中的列值。下面的两张图像显示了 MigrationHistory
表中的列值以及代码中迁移的文件名是相似的。
如果您添加了新的迁移,将会创建一个具有唯一时间戳名称的新文件,并且当您运行 update migration 时,将在 __MigrationHistory
表中为该文件插入一个新行,其列值与所添加文件的名称相同。
结论
在此,我们详细了解了如何利用 Entity Framework 的 Code First 方法,并根据需要使用它们。我使用基本的控制台应用程序来解释这个概念,但这些也可以用于任何使用 WebAPIs、ASP.NET 项目或 MVC 项目的企业级应用程序。我们还详细了解了 Code First 迁移以及迁移表的重要性。在此处下载完整的免费电子书(深入了解 Microsoft .NET Entity Framework),了解 Entity Framework 此处。