當 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);
即可避免數據庫被自動創建或自動遷移。