在EntityFramework的开发过程中我们有时因需求变化或者数据结构设计的变化经常会改动表结构。但数据库Schema发生变化时EF会要求我们做DataMigration 和UpdateDatabase。但在这个过程中如何才能保证现有数据库的数据存在。
另外本文只针对CodeFirst的方式来做。
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
现在我想为我们的 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
运行完命令后我们会发现Project树中多了一个 Migrations folder to our project, this new folder contains two files:
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 命令时生成。
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.
Code First Migrations 有两个主要的命令需要熟悉.
我们生成的Migration需要注意添加了个新的Url属性,Add-Migration 允许我们给Migration 名字,这里我指定为 AddBlogUrl.
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应用到数据库.
最终 MigrationsDemo.BlogContext 数据库中的Blogs表会增加 Url 列.
到目前位置 我们生成并执行了一个没有做任何修改的Migration. 现在我们看下如何通过修改自动生成的代码实现自定义Migration.
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; } }
public virtual List<Post> Posts { get; set; }
我们将用Add-Migration 生成 migration,并命名为AddPostClass.
Code First Migrations在组织修改时的确做的不错, 但我还是想做点修改:
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的执行情况.
到目前我们了解了到Migration的操作不会修改数据也不会转移数据到新数据库,现在就让我们看下如何搬运我们的数据,这里没有原生支持,但我们可以在通过在指定的位置执行任意想要的SQL脚本来实现我们的想法。
添加 Post.Abstract 属性. 稍后我们将用 Content的内容来生成Abstract .
public string Abstract { get; set; }
我们将用 Add-Migration 命令让Code First Migrations 为我们生成最接近我们想要migration.
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的执行情况.
到目前为止我们已经可以将数据库更新到最新版本Migration,但有时需要升级或降级到指定的Migration.
现在有个特殊情况想让数据库回到运行完AddBlogUrl migration的状态。这事我们就可以使用–TargetMigration 来降级数据库到该版本
该命令将会运行 AddBlogAbstract 和AddPostClass migration的 Down脚本 .
如果想回到最初 Update-Database –TargetMigration: $InitialDatabase 这条命令将会帮你一步到位.
如果另外一个开发人员想要应用我们对数据库的修改,他能通过Source Control 获取我签入的最新代码,一旦他有了最新代码就可以通过Update-Database 将所有修改应用到本地。然而有时我们想把我们的修改发布到测试服务器甚至生产环境中,这样我们或许就需要一份可以交给DBA的SQL脚本。
Code First Migrations 将会把实际要应用的修改生成一个.sql文件而不是应用到数据库。一旦SQL脚本生成VisualStudio会自动打开该脚本,你可以选择是否保存。
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 的好处