引言
在EntityFramework的开发过程中我们有时因需求变化或者数据结构设计的变化经常会改动表结构。但数据库Schema发生变化时EF会要求我们做DataMigration 和UpdateDatabase。但在这个过程中如何才能保证现有数据库的数据存在。
另外本文只针对CodeFirst的方式来做。
准备一个简单的 EFCodeFirst Demo
- 创建一个控制台程序 MigrationsDemo
- NuGet 获取最新版 EntityFramework
- Tools –> Library Package Manager –> Package Manager Console
- Run the Install-Package EntityFramework command
- 添加 Model.cs文件. 定义一个 Blog 类作为我们的业务模型 并为其添加 BlogContext 使用 EF (Code First )模式的DataContext
using System.Data.Entity; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity.Infrastructure; namespace MigrationsDemo { public class BlogContext : DbContext { public DbSet<Blog> Blogs { get; set; } } public class Blog { public int BlogId { get; set; } public string Name { get; set; } } }
在Program.cs文件中简单演示下如何使用我们刚才定义好的业务类和DataContext。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MigrationsDemo { class Program { static void Main(string[] args) { using (var db = new BlogContext()) { db.Blogs.Add(new Blog { Name = "Another Blog " }); db.SaveChanges(); foreach (var blog in db.Blogs) { Console.WriteLine(blog.Name); } } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }
迫不及待的运行下我们的小程序:
运行程序后我们可以查看下EF为我们自动创建的MigrationsCodeDemo.BlogContext 数据库:
If SQL Express is installed (included in Visual Studio 2010) then the database is created on your local SQL Express instance (.SQLEXPRESS). If SQL Express is not installed then Code First will try and use LocalDb ((localdb)v11.0) - LocalDb is included with Visual Studio 2012.
Note: SQL Express will always get precedence if it is installed, even if you are using Visual Studio 2012
启用 Migration
现在我想为我们的 Blog 类增加一个URL属性。
在Blog类增加如下一行代码:
public string Url { get; set; }
再次运行我们的程序!
Ops…出问题啦!!
If you were to run the application again you would get an InvalidOperationException stating The model backing the 'BlogContext' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).
根据异常信息提示,我们要为DataContext启用Migrations
- 在Package Manager Console 窗口中 运行命令 Enable-Migrations
运行完命令后我们会发现Project树中多了一个 Migrations folder to our project, this new folder contains two files:
- The Configuration class. 这个类用于自定义Migration的行为. 这里我们使用默认Configuration.
我们项目中只是一个简单的 Code First context , Enable-Migrations 自动将context type 应用到 configuration 的泛型类型参数. - An InitialCreate migration. 因为我们在启用migration之前已经有Code First 数据库,所以就会生成这个migration。 这个migration 的代码表示了数据库中已有的 objects . 在该例中 就是 Blog 表有 BlogId 和 Name 两列. 文件名中含有一个时间戳,这样方便我们排序。
If the database had not already been created this InitialCreate migration would not have been added to the project. Instead, the first time we call Add-Migration the code to create these tables would be scaffolded to a new migration.
如果刚开始没有创建数据库,InitialCreate migration将会在第一次执行 Add-Migration 命令时生成。
同一个数据库同时对应多个Model
When using versions prior to EF6, only one Code First model could be used to generate/manage the schema of a database. This is the result of a single __MigrationsHistory table per database with no way to identify which entries belong to which model.
Starting with EF6, the Configuration class includes a ContextKey property. This acts as a unique identifier for each Code First model. A corresponding column in the __MigrationsHistorytable allows entries from multiple models to share the table. By default, this property is set to the fully qualified name of your context.
如何生成和运行Migration
Code First Migrations 有两个主要的命令需要熟悉.
- Add-Migration 组织基于上次Migration后的一切修改生成这次的Migration。
- Update-Database 将所有挂起(未执行)的migration应用到数据库。
我们生成的Migration需要注意添加了个新的Url属性,Add-Migration 允许我们给Migration 名字,这里我指定为 AddBlogUrl.
- 在Package Manager Console 中执行 Add-Migration AddBlogUrl 命令
- 在Migrations文件夹中生成了一个 AddBlogUrl migration.并且自动为名字添加了个时间戳,方便我们排序。
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddBlogUrl : DbMigration { public override void Up() { AddColumn("dbo.Blogs", "Url", c => c.String()); } public override void Down() { DropColumn("dbo.Blogs", "Url"); } } }
我们可以编辑这个 migration,但这里我们无需做任何修改 .该是使用 Update-Database 将Migration应用到数据库.
- 在Package Manager Console 运行 Update-Database 命令
- Code First Migrations 将把Migrations 文件夹中的Migration于已经应用到数据库中Migration进行比较。 最终我们会看到AddBlogUrl 将会被应用数据库.
最终 MigrationsDemo.BlogContext 数据库中的Blogs表会增加 Url 列.
自定义 Migration
到目前位置 我们生成并执行了一个没有做任何修改的Migration. 现在我们看下如何通过修改自动生成的代码实现自定义Migration.
- 为 Blog 添加 Rating 属性
public int Rating { get; set; }
我们增加一个Post类
public class Post { public int PostId { get; set; } [MaxLength(200)] public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
- 为Blog 类增加一个 Posts collection 属性来组织Blog和Post的关系
public virtual List<Post> Posts { get; set; }
我们将用Add-Migration 生成 migration,并命名为AddPostClass.
- 在Package Manager Console运行 Add-Migration AddPostClass .
Code First Migrations在组织修改时的确做的不错, 但我还是想做点修改:
- 首先 为 Posts.Title 列增加一个unique index
- 增加非空列 Blogs.Rating . 如果数据表中已经有数据,会给新增的该列一个CLR中该类型的默认值(因为Rating 是integer所以设为 0),但我们想为已经存在的数据,设置Rating 默认值为3 .
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddPostClass : DbMigration { public override void Up() { CreateTable( "dbo.Posts", c => new { PostId = c.Int(nullable: false, identity: true), Title = c.String(maxLength: 200), Content = c.String(), BlogId = c.Int(nullable: false), }) .PrimaryKey(t => t.PostId) .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true) .Index(t => t.BlogId) .Index(p => p.Title, unique: true); AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3)); } public override void Down() { DropIndex("dbo.Posts", new[] { "Title" }); DropIndex("dbo.Posts", new[] { "BlogId" }); DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs"); DropColumn("dbo.Blogs", "Rating"); DropTable("dbo.Posts"); } } }
我们修改migration来满足我们的需求 ,运行Update-Database 来升级数据库到最新版本. 我们可以通过指定 –Verbose 看到 SQL的执行情况.
- 在 Package Manager Console运行 Update-Database –Verbose .
Migration过程中的数据迁移|自定义SQL
到目前我们了解了到Migration的操作不会修改数据也不会转移数据到新数据库,现在就让我们看下如何搬运我们的数据,这里没有原生支持,但我们可以在通过在指定的位置执行任意想要的SQL脚本来实现我们的想法。
添加 Post.Abstract 属性. 稍后我们将用 Content的内容来生成Abstract .
public string Abstract { get; set; }
我们将用 Add-Migration 命令让Code First Migrations 为我们生成最接近我们想要migration.
- 在Package Manager Console运行 Add-Migration AddPostAbstract 命令.
- 生成的migration 注意到schema 的变化,但没能实现用Post的前100个字符content 来填充Abstract 列. 我可以通过SQL 运行UPDATE 命令,在创建Abstract 列.
(Adding in line 12 in the code below)
namespace MigrationsDemo.Migrations { using System; using System.Data.Entity.Migrations; public partial class AddPostAbstract : DbMigration { public override void Up() { AddColumn("dbo.Posts", "Abstract", c => c.String()); Sql("UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL"); } public override void Down() { DropColumn("dbo.Posts", "Abstract"); } } }
我们修改migration来满足我们的需求 ,运行Update-Database 来升级数据库到最新版本. 我们可以通过指定 –Verbose 看到 SQL的执行情况.
- 在 Package Manager Console运行 Update-Database –Verbose .
将数据库更新到指定的版本Migration(包括升级 ,降级或者说是回滚)
到目前为止我们已经可以将数据库更新到最新版本Migration,但有时需要升级或降级到指定的Migration.
现在有个特殊情况想让数据库回到运行完AddBlogUrl migration的状态。这事我们就可以使用–TargetMigration 来降级数据库到该版本
- 在Package Manager Console 运行Update-Database –TargetMigration: AddBlogUrl 命令.
该命令将会运行 AddBlogAbstract 和AddPostClass migration的 Down脚本 .
如果想回到最初 Update-Database –TargetMigration: $InitialDatabase 这条命令将会帮你一步到位.
获取Migration的SQL 脚本
如果另外一个开发人员想要应用我们对数据库的修改,他能通过Source Control 获取我签入的最新代码,一旦他有了最新代码就可以通过Update-Database 将所有修改应用到本地。然而有时我们想把我们的修改发布到测试服务器甚至生产环境中,这样我们或许就需要一份可以交给DBA的SQL脚本。
- 运行Update-Database 的同时指定 –Script 这样就只会生成脚本而不是应用Migration。我们也可以指定Migration的Source和Target。eg. 从空数据库 ($InitialDatabase) 到最新版本(migration AddPostAbstract).
If you don’t specify a target migration, Migrations will use the latest migration as the target. If you don't specify a source migrations, Migrations will use the current state of the database.(如果没有指定目标版本,将会默认设置Target到最新版本Migration,如果没有指定源,将默认将当前数据库的状态设置为Source) - 在Package Manager Console中Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: AddPostAbstract
Code First Migrations 将会把实际要应用的修改生成一个.sql文件而不是应用到数据库。一旦SQL脚本生成VisualStudio会自动打开该脚本,你可以选择是否保存。
Generating Idempotent Scripts (EF6 onwards)
Starting with EF6, if you specify –SourceMigration $InitialDatabase then the generated script will be ‘idempotent’. Idempotent scripts can upgrade a database currently at any version to the latest version (or the specified version if you use –TargetMigration). The generated script includes logic to check the __MigrationsHistory table and only apply changes that haven't been previously applied.
应用程序启动时自动升级数据库导最新版本
在部署程序后,如果想在程序启动时自动升级数据库(通过应用Migration),你可以通过注册MigrateDatabaseToLatestVersion database initializer来实现该目的. 它是一个简单的 database initializer 但能确保数据库正确升级到最新版本. This logic is run the first time the context is used within the application process (AppDomain).
修改下Program.cs 如下示, 给BlogContext设置MigrateDatabaseToLatestVersion initializer . 注意需要引入 System.Data.Entity 命名空间
When we create an instance of this initializer we need to specify the context type (BlogContext) and the migrations configuration (Configuration) - the migrations configuration is the class that got added to our Migrations folder when we enabled Migrations.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity; using MigrationsDemo.Migrations; namespace MigrationsDemo { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<BlogContext, Configuration>()); using (var db = new BlogContext()) { db.Blogs.Add(new Blog { Name = "Another Blog " }); db.SaveChanges(); foreach (var blog in db.Blogs) { Console.WriteLine(blog.Name); } } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }
现在我们的程序就可以在启动时,自动将数据库升级到最新版本啦。
源码下载 MigrationsDemo.rar
参考
Code First Migrations in Team Environments
Entity Framework 6 中 Code First 的好处