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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (14投票s)

2018 年 9 月 28 日

CPOL

14分钟阅读

viewsIcon

32257

downloadIcon

847

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 的理论、历史和用法。该系列的议题如下。

代码优先方法

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 方法详解

  1. 创建一个名为 EF_CF 的新控制台应用程序。这将为您提供 Program.cs 文件和一个包含 Main() 方法的文件。

  2. 现在我们将创建模型类,即 POCO(Plain Old CLR Object)类。例如,假设我们要创建一个应用程序,其中将对 employee 进行数据库操作,并且 employee 将被分配到一个 department。因此,一个 department 可以有多个 employees,而一个 employee 只能有一个 department。因此,我们将创建前两个实体 EmployeeDepartment。向项目中添加一个名为 Employee 的新类,并为其添加两个简单的属性,即 EmployeeIdEmployeeName

  3. 类似地,添加一个名为 Department 的新类,并添加 DepartmentIdDepartmentNameDepartmentDescription 属性,如下所示。

  4. 由于一个 employee 属于一个 department,每个 employee 都将有一个相关的 department,因此向 Employee 类添加一个名为 DepartmentId 的新属性。

  5. 现在是时候将 EntityFramework 添加到我们的项目中了。打开程序包管理器控制台,选择您的当前控制台应用程序作为默认项目,然后安装 Entity Framework。我们之前已经执行过几次了,所以现在安装它不会有问题。

  6. 由于我们是从头开始做所有事情,所以我们也需要 DbContext 类。在 Model First 和 Database First 方法中,我们已经生成了 DB context 类。但在这种情况下,我们需要手动创建它。向项目添加一个名为 CodeFirstContext 的新类,该类继承自 namespace System.Data.EntityDbContext 类,如下面的图像所示。现在添加两个名为 EmployeesDepartmentsDbSet 属性,如下面的图像所示。

    最终代码可能如下所示:

    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; }
        }
    }

    DbContextDbSet 都是我们的超级英雄,它们创建和处理数据库操作,并为我们提供极大的便利,使我们能够高度抽象。

    当我们使用 DbContext 时,我们实际上是在处理实体集。DbSet 代表一个类型化的实体集,用于执行创建、读取、更新和删除操作。我们不会独立创建和使用 DbSet 对象。DbSet 只能与 DbContext 一起使用。

  7. 让我们尝试使我们的实现更抽象一些,而不是直接从控制器访问 dbContext,让我们将其抽象到一个名为 DataAccessHelper 的类中。这个类将是我们所有数据库操作的辅助类。因此,向项目添加一个名为 DataAccessHelper 的新类。

  8. 创建 DB context 类的只读实例,并添加一些方法,例如 FetchEmployees() 来获取员工详细信息,FetchDepartments() 来获取部门详细信息。每种方法一个,用于添加 employee 和添加 department。您可以根据需要添加更多方法,例如 updatedelete 操作。目前,我们将仅限于这四种方法。

    代码可能如下所示:

    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;
            }
        }
    }
  9. 现在让我们添加导航属性的概念。导航属性是指类的属性,通过这些属性,可以在 Entity Framework 中通过该实体访问相关实体。因此,在获取 Employee 数据时,我们可能需要获取其相关 Departments 的详细信息;在获取 Department 数据时,我们可能需要获取与之关联的 employees 的详细信息。Navigation 属性以 virtual 属性的形式添加到实体中。因此,在 Employee 类中,添加一个返回单个 Department 实体的 Departments 属性,并将其设为 virtual。类似地,在 Department 类中,添加一个名为 Employees 的属性,该属性返回 Employee 实体的集合,并将其设为 virtual

    以下是 EmployeeDepartment 模型的代码。

    员工

    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; }
        }
    }
  10. 让我们编写一些代码来使用我们的代码执行数据库操作。因此,在 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。但是这一切是如何完成的呢,我们还没有数据库。☹

  11. 不用担心,让我们看看如何确保我们从代码创建了 DB。首先,正如我们之前所见,我们的 context 类名应与我们的连接字符串名称相同,反之亦然。因此,在 App.config 文件中添加一个与 DB context 类同名的连接字符串,如下所示。

    工作完成了!Entity Framework 将处理其余的数据库创建工作。我们只需运行应用程序,现在,当首次使用 DB context 类执行 DB 操作时,就会创建我们的数据库。

  12. main 方法上设置断点并运行应用程序。

  13. 一旦执行到我们编写 AddDepartment 代码的那一行,我们的数据库就会被创建。

  14. 转到数据库服务器,查看我们创建的数据库,其名称与我们在连接 string 中提供的名称相同。我们有 DepartmentsEmployees 表,以及一个名为 __MigrationHistory 的表,用于跟踪在此数据库上执行的 Code First 迁移的历史记录。

    我们看到数据库中还添加了一个 Department,其名称为“Technology”,这是我们在代码中使用的。

    并且,我们的 employee 表已填充了三行,其中包含三个 employees,它们的 department id 为 1,即新添加的 departmentid。因此,我们的 Code First 方法也奏效了。😊

  15. 您可以按 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 迁移,就像我们为其他方法所做的那样。

  1. 添加一个名为 EF_CF_Migrations 的新控制台应用程序。

  2. 添加 Department 模型,属性包括 DepartmentIdDepartmentNameDepartmentDescription。添加一个 virtual 属性作为 navigation 属性,名为 Employees,因为一个 department 可以有多个 employees

  3. 类似地,添加一个名为 Employee 的模型类,并添加三个属性 EmployeeIdEmployeeNameDepartmentId,以及 Departments 作为 navigation 属性,因为一个 employee 可能与任何 department 相关联。

  4. 从程序包管理器控制台安装 Entity Framework,如下面的图像所示。

  5. 添加一个从 DbContext 类派生的 context 类,并在类中将 EmployeeDepartment 类添加为 DbSet 属性。

  6. 现在,执行名为“Enable-Migrations”的命令,但在此之前,请将您的新添加的项目设置为默认项目。必须使用程序包管理器控制台执行该命令。

  7. 命令执行后,您将在应用程序中看到一个名为“Migrations”的文件夹,并且默认会添加一个名为 Configuration 的类,该类包含您的初始配置以及您希望与 Code First 方法一起使用的所有其他配置。您可以在此类的构造函数中配置设置。此类继承自 DbMigrationsConfigurations,它在基类中有一个 virtual 方法 Seed。我们可以在派生类中重写该方法,以便在创建数据库时向数据库添加一些种子数据。

  8. Seed 方法将 context 作为参数。Context 是我们的 CodeFirstContext 类的实例。现在向 context 添加示例数据,例如,如下所示,我正在添加一个名为 Technologydepartment,其中包含三个 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);
            }
        }
    }

  9. 现在,在程序包管理器控制台执行另一个命令,称为“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");
            }
        }
    }
  10. 在我们继续之前,还有一个需要弥合的差距。我们需要在 App.config 中有一个与我们的 context 类同名的连接字符串。因此,打开项目的 app.config 文件,并根据需要添加连接字符串,其中包含服务器和数据库名称详细信息。

  11. 迁移的最后一步是执行一个名为“Update-Database”的命令。

    在程序包管理器控制台上执行此命令时,它将应用我们在 Migrations 文件夹下的所有迁移,并运行 Configuration 类的 seed 方法。

  12. 现在,转到数据库检查是否已创建表以及其中是否包含我们在 seed 方法中提供的示例数据。在下面的图像中,我们看到 Departments 表包含我们在 seed 方法中作为 Department 模型添加到 context 的示例 department

    Employees 表中,我们有所有与该 department 相关联的 employees,以及我们通过 seed 方法添加的另一个额外的 employee

  13. 让我们在 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();
            }
        }
    }
  14. 按 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 此处

© . All rights reserved.