使用 Code First 數據庫遷移

當 Entity Framework Code First 的數據模型發生改變時,默認會引發一個System.InvalidOperationException 的異常。解決方法是使用DropCreateDatabaseAlways 或DropCreateDatabaseIfModelChanges,讓Entity Framework 自動將數據庫刪除,然後重新創建。不過,這種方式過於殘暴,應該使用更人性化的方式,讓Entity Framework 幫助我們自動調整數據庫架構。並且仍然保留現有數據庫中的數據。而這種開發技術就是 Code First 數據庫遷移(DB Migration)。

首先,我們先用 Code First 方式建立一個簡單的ASP.NET MVC4 應用程序

在Models 文件夾下建立兩個實體類Member、Guestbook。

Member 實體類定義如下:

namespace CodeFirstDemo.Models
{
    public partial class Member
    {
        public Member()
        {
            this.Guestbooks = new List<Guestbook>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public virtual ICollection<Guestbook> Guestbooks { get; set; }
    }
}

Guestbook 實體類定義如下:

namespace CodeFirstDemo.Models
{
    public partial class Guestbook
    {
        public int Id { get; set; }
        public string Message { get; set; }
        public System.DateTime CreatedOn { get; set; }
        public int MemberId { get; set; }
        public virtual Member Member { get; set; }
    }
}

在Models 文件夾下建立Mapping 文件夾,並建立對應實體類的關係映射類MemberMap 、GuestbookMap

 

MemberMap 類定義如下:

namespace CodeFirstDemo.Models.Mapping
{
    public class MemberMap : EntityTypeConfiguration<Member>
    {
        public MemberMap()
        {
            // Primary Key
            this.HasKey(t => t.Id);

            // Properties
            this.Property(t => t.Id)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            this.Property(t => t.Name)
                .IsRequired()
                .HasMaxLength(10);

            this.Property(t => t.Email)
                .IsRequired()
                .HasMaxLength(200);

            // Table & Column Mappings
            this.ToTable("Member");
            this.Property(t => t.Id).HasColumnName("Id");
            this.Property(t => t.Name).HasColumnName("Name");
            this.Property(t => t.Email).HasColumnName("Email");
        }
    }
}

GuestbookMap 類定義如下:

namespace CodeFirstDemo.Models.Mapping
{
    public class GuestbookMap : EntityTypeConfiguration<Guestbook>
    {
        public GuestbookMap()
        {
            // Primary Key
            this.HasKey(t => t.Id);

            // Properties
            this.Property(t => t.Message)
                .IsRequired()
                .HasMaxLength(200);

            // Table & Column Mappings
            this.ToTable("Guestbook");
            this.Property(t => t.Id).HasColumnName("Id");
            this.Property(t => t.Message).HasColumnName("Message");
            this.Property(t => t.CreatedOn).HasColumnName("CreatedOn");
            this.Property(t => t.MemberId).HasColumnName("MemberId");

            // Relationships
            this.HasRequired(t => t.Member)
                .WithMany(t => t.Guestbooks)
                .HasForeignKey(d => d.MemberId);

        }
    }
}

在Models 建立數據庫上下文類CodeFirstDemoContext

CodeFirstDemoContext 類定義如下:

namespace CodeFirstDemo.Models
{
    public partial class CodeFirstDemoContext : DbContext
    {
        static CodeFirstDemoContext()
        {
            //Database.SetInitializer<CodeFirstDemoContext>(new DropCreateDatabaseIfModelChanges<CodeFirstDemoContext>());
        }

        public CodeFirstDemoContext()
            : base("Name=CodeFirstDemoContext")
        {
        }

        public DbSet<Guestbook> Guestbooks { get; set; }
        public DbSet<Member> Members { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new GuestbookMap());
            modelBuilder.Configurations.Add(new MemberMap());
        }
    }
}

Models 文件夾結構下

以上就是一個簡單的 Code First 結構了

接下來在Web.config 添加數據庫連接字符串

<connectionStrings>
    <add name="CodeFirstDemoContext" connectionString="Data Source=vin-pc;Initial Catalog=CodeFirstDemo;Persist Security Info=True;User ID=sa;Password=123456;MultipleActiveResultSets=True"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

然後添加一個控制器HomeController

namespace CodeFirstDemo.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            CodeFirstDemoContext db = new CodeFirstDemoContext();
            Member member = new Member { Name = "tt", Email = "[email protected]" };
            db.Members.Add(member);
            db.SaveChanges();

            return View();
        }

    }
}

EF Code First 如何記錄版本

當應用程序通過EF Code First 創建數據庫後,在此數據庫中講會自動創建一個名爲 dbo. __MigrationHistory 的系統數據表,如下圖所示:

打開dbo. __MigrationHistory 會發現三個字段:MigrationId 字段用來記錄這次由 EFCode First 所創建的一個表示名稱,也可以稱爲一個版本代碼;Model 字段表示這次創建時的模型數據,這是由 Entity Framework 將所有數據模型串行化後的版本,所以看不出是什麼;ProductVersion 字段表示當前使用的Entity Framework 版本,如下圖所示:

如果尚未啓用數據庫遷移功能,每次在應用程序運行時,都會對比程序中當前的數據模型,與數據庫中dbo. __MigrationHistory 表的Model 字段中的值是否一致,如果不一致,默認就會發生異常。

如果啓用數據庫遷移功能之後,這個表就會開始記錄每次數據模型變動的記錄與版本。

啓用數據庫遷移

若要在項目中啓用數據庫遷移功能,必須先開啓程序包管理器控制檯(Package Manager Console)窗口,然後輸入 Enable-Migrations指令,如下圖:


運行 Enable-Migrations 指令的過程中, Visual Studio 會在項目裏創建一個Migrations 目錄,該目錄下還創建有兩個文件,201309120825043_InitialCreate.cs 、Configuration.cs,如下圖:

1.      201309120825043_InitialCreate.cs

在啓用數據庫遷移之前,由於已經通過 Code First 在數據庫中創建好了相關的數據庫結構,也創建了一個初始的dbo. __MigrationHistory 數據表,表中也有一條數據,這條數據的MigrationId值正好會等於文檔名。VS會將dbo. __MigrationHistory 表的Model 值讀出,並創建這個類的屬性,其屬性就是包含那次數據模型的完整描述。

namespace CodeFirstDemo.Migrations
{
    using System;
    using System.Data.Entity.Migrations;
    
    public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Guestbook",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Message = c.String(nullable: false, maxLength: 200),
                        CreatedOn = c.DateTime(nullable: false),
                        MemberId = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.Member", t => t.MemberId, cascadeDelete: true)
                .Index(t => t.MemberId);
            
            CreateTable(
                "dbo.Member",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 5),
                        Email = c.String(nullable: false, maxLength: 200),
                    })
                .PrimaryKey(t => t.Id);
            
        }
        
        public override void Down()
        {
            DropIndex("dbo.Guestbook", new[] { "MemberId" });
            DropForeignKey("dbo.Guestbook", "MemberId", "dbo.Member");
            DropTable("dbo.Member");
            DropTable("dbo.Guestbook");
        }
    }
}

2.      Configuration.cs

這個類定義了運行數據庫遷移時該有的行爲。默認情況下,數據庫並不會發生遷移動作,除非將 Configuration() 內的 AutomaticMigrationsEnabled 改爲 true,纔會讓 CodeFirst 自動遷移數據庫。

namespace CodeFirstDemo.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<CodeFirstDemo.Models.CodeFirstDemoContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(CodeFirstDemo.Models.CodeFirstDemoContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }
}

運行數據庫遷移

下面來更改 Member 的數據模型,添加兩個字段,分別是 UserName 和 Password 屬性。

namespace CodeFirstDemo.Models
{
    public partial class Member
    {
        public Member()
        {
            this.Guestbooks = new List<Guestbook>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public virtual ICollection<Guestbook> Guestbooks { get; set; }
    }
}

通過Package Manager Console 輸入 Add-Migration 指令,來新增一條數據庫遷移版本,輸入時必須帶上一個“版本名稱”參數。例如,想要取名爲AddUsernamePassword,則可以輸入以下指令:

運行完成後,會在 Migrations 文件夾新增一個文件,如下圖:

這次運行 Add-Migration 指令,所代表的意思就是新增一次運行數據庫遷移命令,VS2012會自動對比當前數據庫中的 Model 定義與當前更改過的數據模型,並將差異的字段變化寫入這個自動新增的類內,程序代碼如下:

namespace CodeFirstDemo.Migrations
{
    using System;
    using System.Data.Entity.Migrations;
    
    public partial class AddUsernamePassword : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Member", "UserName", c => c.String());
            AddColumn("dbo.Member", "Password", c => c.String());
        }
        
        public override void Down()
        {
            DropColumn("dbo.Member", "Password");
            DropColumn("dbo.Member", "UserName");
        }
    }
}

NOTES

每一次新增數據庫遷移版本,其類內都會包含一個Up() 方法與 Down() 方法,所代表的意思分別是“升級數據庫”與“降級數據庫”的動作,所以,數據庫遷移不僅僅只是將數據庫升級,還可以恢復到舊版本。

當前還沒有對數據庫做任何遷移動作,所以數據庫中的數據結構並沒有任何改變,現在,手動在 Member 數據表中輸入幾條數據,以確認待會兒數據庫遷移(升級)之後數據是否消失,如圖:

接着,對數據庫進行遷移動作,在程序包管理控制檯(Package Manager Console)窗口中輸入Update-Database指令,如圖:

更新數據庫成功之後,可以查看 Member  數據表結構是否發生變化,以及數據表原來的數據是否存在:

NOTES

我們都知道,在客戶端數據庫通常是無法直接聯機的,客戶的生產環境通常也沒有安裝VS2012,那麼如果數據庫遷移動作要進行套用時,應該怎麼辦呢?可以通過 Update-Database 指令的其他參數自動生產數據庫遷移的 T-SQL 腳本,然後攜帶 T-SQL 腳本文件到正式主機部署或更新即可。

Update-Database 指令的–SourceMigration 參數可以指定來源斑斑駁駁,-Targetigration 參數可以指定目標版本, -Script 參數可以用來輸出 T-SQL 腳本。以下是生成本次數據庫遷移(升級)的 T-SQL 指令演示:

Update-Database –SourceMigration201309120825043_InitialCreate –TargetMigration 201309130055351_AddUsernamePassword-Script

如果要生成數據庫降級的 T-SQL,則不能使用–SourceMigration 參數,直接指定–TargetMigration 參數即可,演示如下:

Update-Database –TargetMigration201309120825043_InitialCreate –Script

如果要還原數據庫帶添加 Code First 之前的初始狀態,可以輸入以下指令:

Update-Database  -TragetMigration:$InitialDatabase –Script

自定義數據庫遷移規則

當了解數據庫遷移的規則之後,如果希望在數據庫遷移的過程中進行一些微調,例如, Entity Framework 並不支持自動設置字段的默認值,假設我們在 Member 數據模型中想添加一個新的 CreatedOn 屬性表示會員的註冊日期,並且希望在數據庫中自動加上 getdate() 默認值,這時就必須要自定義數據庫遷移的規則。

首先更改 Member 數據模型,加上 CreatedOn 屬性

 

 

Member.cs

namespace CodeFirstDemo.Models
{
    public partial class Member
    {
        public Member()
        {
            this.Guestbooks = new List<Guestbook>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public DateTime CreatedOn { get; set; }
        public virtual ICollection<Guestbook> Guestbooks { get; set; }
    }
}

MemberMap.cs

namespace CodeFirstDemo.Models.Mapping
{
    public class MemberMap : EntityTypeConfiguration<Member>
    {
        public MemberMap()
        {
            // Primary Key
            this.HasKey(t => t.Id);

            // Properties
            this.Property(t => t.Id)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            this.Property(t => t.Name)
                .IsRequired()
                .HasMaxLength(10);

            this.Property(t => t.Email)
                .IsRequired()
                .HasMaxLength(200);

            this.Property(t => t.CreatedOn)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);

            // Table & Column Mappings
            this.ToTable("Member");
            this.Property(t => t.Id).HasColumnName("Id");
            this.Property(t => t.Name).HasColumnName("Name");
            this.Property(t => t.Email).HasColumnName("Email");
        }
    }
}

然後運行一次 Add-Migration指令,並指定版本名稱爲 AddMemberCreatedOn

這時,再 Migrations 目錄下多出一個201309130144538_AddMemberCreatedOn.cs 文件

namespace CodeFirstDemo.Migrations
{
    using System;
    using System.Data.Entity.Migrations;
    
    public partial class AddMemberCreatedOn : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Member", "CreatedOn", c => c.DateTime(nullable: false));
        }
        
        public override void Down()
        {
            DropColumn("dbo.Member", "CreatedOn");
        }
    }
}

這次我們用不一樣的參數來運行數據庫遷移,加上–Script 參數,Update-Database –Script

運行完後,會輸出完整的數據庫更新 T-SQL 腳本,其中第一行就是在 Member 數據表中新增一個 CreatedOn 字段,而且會看到該字段已經給予‘1900-01-01T00:00:00.000’ 這個默認值。第二行則是在 _MigrationHistory新增一條版本記錄,如下圖:

此時,可以自定義201309130144538_AddMemberCreatedOn.cs 類裏的 Up() 方法,在新增字段的地方改用Sql()方法,傳入一段自定義的 T-SQL 腳本來創建字段,並改用自己的方法新增字段,如此一來,即可讓數據庫遷移在升級是自動加上此字段的默認值。

public override void Up()
        {
            //AddColumn("dbo.Member", "CreatedOn", c => c.DateTime(nullable: false));
            Sql("ALTER TABLE [dbo].[Member] ADD [CreatedOn] [datetime] NOT NULL DEFAULT getdate()");
        }

最後,運行 Update-Database 指令,這是再去檢查 Member 數據表,可以看到,數據庫遷移升級後的 CreatedOn 字段擁有了我們想要的 getdate() 默認值,如下圖:

TIPS

在數據庫遷移類中除了有 Up() 方法外,還有 Down() 方法,必須留意當降級時必要的架構的變更動作,如果自定義數據庫遷移的規則寫不好,可能會導致降級失敗或數據庫結構紊亂

 

自動數據庫遷移

如果要啓用自動數據庫遷移的話,在Database.SetInitializer() 方法中使用System.Data.Entity.MigrateDatabaseToLatestVersion泛型類型,並且傳入兩個參數,第一個是數據上下文類,第二個是在啓用數據庫遷移時自動生成的 Configuration 類,這個類餵魚 Migrations 目錄下,所以記得要加上命名空間:

Database.SetInitializer(new MigrateDatabaseToLatestVersion<CodeFirstDemoContext, Migrations.Configuration>());

接着再開啓Migrations\Configuration.cs 設置AutomaticMigrationsEnbaled 屬性爲 ture 即可

AutomaticMigrationsEnabled = true;

如此一來,日後所以的數據模型變動時,都會通過數據庫遷移功能自動升級數據庫,當每次自動升級發生時,也會在 dbo._MigrationHistory 系統數據表裏記錄,並以AutomaticMigration 命名,如下圖:

如何避免數據庫被自動創建或自動遷移

如果想要避免數據庫被自動創建或自動遷移,則修改Database.SetInitializer() 方法,如:

Database.SetInitializer<CodeFirstDemoContext>(null);

即可避免數據庫被自動創建或自動遷移。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章