原文:Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application
1.启用Code First迁移:
当我们开发一个新的程序时,数据模型经常会发生改变,每次模型发生改变时,就会变得与数据库不同步。我们之前配置EF在每次数据模型发生改变时自动删除然后重建数据库。当我们增加、删除或者改变实体类或者改变DbContext类时,在程序下次运行时将会自动删除已经存在的数据库,并且创建一个匹配模型的数据库,并添加测试数据。
这种方式在我们部署程序之前不会用什么问题。当程序在产品上运行时,当我们改变模型(比如新增一列)时,我们不希望每次都丢失所有数据。Code First迁移特性可以让EF更新数据库架构而不是删除重建数据库。
1.1.1.禁用之前在Web.config添加的初始化配置:
<!--<contexts> <context type="ContosoUniversity.DAL.SchoolContext, ContosoUniversity"> <databaseInitializer type="ContosoUniversity.DAL.SchoolInitializer, ContosoUniversity" /> </context> </contexts>-->
1.1.2.将连接数据库名改为ContosoUniversity2:
<connectionStrings> <add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> </connectionStrings>
这样设置程序在第一次迁移时将会创建一个新的数据库。这个步骤不是必须的,但是我们稍后会看到这是一个比较好的做法。
1.1.3.在Package Manager Console输入下面两条命令:
enable-migrations add-migration InitialCreate
enable-migrations命令在项目中创建了一个Migrations文件夹,并在该文件夹添加了Configuration.cs文件,可以编辑该文件配置迁移。
(如果我们没有按照上面的操作修改连接数据库的名字,迁移命令会自动找到已经存在的数据库并自动添加add-migration命令。这样也是可以的,它只是意味着在我们部署数据库之前不会运行迁移代码的测试。稍后当我们执行update-database
命令时,什么操作都不会发生,因为数据库已经存在)
像之前的SchoolInitializer类一样,Configuration
类也有一个Seed
方法。Seed
方法的目的是让我们在Code First新建或更新数据库后能够插入或者更新测试数据。这个方法在数据库创建和每次因为数据模型改变而数据库架构发生改变时都会被调用。
internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(ContosoUniversity.DAL.SchoolContext 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" } // ); // } }
1.2.修改Seed方法:
在每次数据模型发生删除重建数据库时,我们使用Seed方法插入测试数据,因为每次删除数据库时所有的测试数据都会丢失。但是使用Code First迁移,在数据库发生改变时,测试数据依然存在,因此在Seed方法中添加测试数据通常是没有必要的。实际上,当我们使用迁移将数据库部署在产品上的时候,我们不想使用Seed方法插入数据,因为Seed方法会在产品运行。在这种情况下,除非测试数据是产品需要的,我们才希望运行Seed方法插入数据。例如,当程序部署为产品时,我们可能需要数据库的Department表中包含实际的学院信息。
在本教程中,我们将会使用迁移来发布程序,但是为了更容易看到程序的工作方式,而无需手动插入大量的数据,Seed方法将会插入测试数据。
1.2.1.修改Configuration.cs的Seed方法,在新建数据库时插入测试数据:
protected override void Seed(ContosoUniversity.DAL.SchoolContext context) { var students = new List<Student> { new Student { FirstMidName = "Carson", LastName = "Alexander", EnrollmentDate = DateTime.Parse("2010-09-01") }, new Student { FirstMidName = "Meredith", LastName = "Alonso", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Arturo", LastName = "Anand", EnrollmentDate = DateTime.Parse("2013-09-01") }, new Student { FirstMidName = "Gytis", LastName = "Barzdukas", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Yan", LastName = "Li", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Peggy", LastName = "Justice", EnrollmentDate = DateTime.Parse("2011-09-01") }, new Student { FirstMidName = "Laura", LastName = "Norman", EnrollmentDate = DateTime.Parse("2013-09-01") }, new Student { FirstMidName = "Nino", LastName = "Olivetto", EnrollmentDate = DateTime.Parse("2005-08-11") } }; students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s)); context.SaveChanges(); var courses = new List<Course> { new Course {CourseID = 1050, Title = "Chemistry", Credits = 3, }, new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3, }, new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3, }, new Course {CourseID = 1045, Title = "Calculus", Credits = 4, }, new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4, }, new Course {CourseID = 2021, Title = "Composition", Credits = 3, }, new Course {CourseID = 2042, Title = "Literature", Credits = 4, } }; courses.ForEach(s => context.Courses.AddOrUpdate(p => p.Title, s)); context.SaveChanges(); var enrollments = new List<Enrollment> { new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, Grade = Grade.A }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, Grade = Grade.C }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Anand").ID, CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID }, new Enrollment { StudentID = students.Single(s => s.LastName == "Anand").ID, CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Barzdukas").ID, CourseID = courses.Single(c => c.Title == "Chemistry").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Li").ID, CourseID = courses.Single(c => c.Title == "Composition").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Justice").ID, CourseID = courses.Single(c => c.Title == "Literature").CourseID, Grade = Grade.B } }; foreach (Enrollment e in enrollments) { var enrollmentInDataBase = context.Enrollments.Where( s => s.Student.ID == e.StudentID && s.Course.CourseID == e.CourseID).SingleOrDefault(); if (enrollmentInDataBase == null) { context.Enrollments.Add(e); } } context.SaveChanges(); }
有些语句使用的是AddOrUpdate来执行插入数据操作。因为Seed方法在我们每次执行update-database
命令时都会运行,通常在每次迁移时,我们不能仅仅插入数据,因为在第一次创建数据库后,视图插入的数据已经存在。AddOrUpdate避免了插入已经存在数据的错误,但是当我们测试程序时它会覆盖会把我们修改的数据覆盖掉。在某些情况下,当我们在测试时修改了一些数据,然而我们希望这些修改在数据库结构发生改变时依然保留。在这种情况下,我们需要怎加一个插入操作的判断条件:仅在该行数据不存在时插入该行数据。Seed方法采用这两种方式。
上面的例子中,LastName
列被用来检查行的唯一性,如果我们添加的测试数据存在LastName
重复的情况,那么下次执行迁移的时候将会得到异常信息:Sequence contains more than one element
关于如何处理冗余数据(redundant data),比如有两个名字为“Alexander Carson”的student数据,请查看Seeding and Debugging Entity Framework (EF) DBs。更多关于AddOrUpdate
方法的信息请查看Take care with EF 4.3 AddOrUpdate Method。
1.2.2.编译一下程序。
1.3.执行第一次迁移:
当我们执行add-migration
命令时,迁移产生的代码将会从头开始新建数据库。产生的代码也是在Migrations文件夹,名字为<timestamp>_InitialCreate.cs。该类中的Up方法,创建于数据模型实体集对应的数据表,Down方法则是把它们删除。
public partial class InitialCreate : DbMigration { public override void Up() { CreateTable( "dbo.Course", c => new { CourseID = c.Int(nullable: false), Title = c.String(), Credits = c.Int(nullable: false), }) .PrimaryKey(t => t.CourseID); CreateTable( "dbo.Enrollment", c => new { EnrollmentID = c.Int(nullable: false, identity: true), CourseID = c.Int(nullable: false), StudentID = c.Int(nullable: false), Grade = c.Int(), }) .PrimaryKey(t => t.EnrollmentID) .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true) .ForeignKey("dbo.Student", t => t.StudentID, cascadeDelete: true) .Index(t => t.CourseID) .Index(t => t.StudentID); CreateTable( "dbo.Student", c => new { ID = c.Int(nullable: false, identity: true), LastName = c.String(), FirstMidName = c.String(), EnrollmentDate = c.DateTime(nullable: false), }) .PrimaryKey(t => t.ID); } public override void Down() { DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student"); DropForeignKey("dbo.Enrollment", "CourseID", "dbo.Course"); DropIndex("dbo.Enrollment", new[] { "StudentID" }); DropIndex("dbo.Enrollment", new[] { "CourseID" }); DropTable("dbo.Student"); DropTable("dbo.Enrollment"); DropTable("dbo.Course"); } }
迁移调用Up方法来实现数据模型的变化迁移。当我们输入回滚命令时,迁移就会调用Down方法。
当我们执行第一次迁移时数据库已经存在,创建数据库的代码也会生成,只是它不一定会执行,因为已存在的数据库已经与数据模型匹配。当我们把程序部署在其他环境中时,这时数据库并不存在,创建数据库的代码将会运行来新建一个数据库,因此在此之前测试下代码的运行情况会比较好。这就是为什么我们之前改变连接字符串中数据库的名字,这样迁移可以从头开始新建数据库。
1.3.1.在Package Manager Console窗口输入命令:update-database
update-database
命令首先调用Up方法创建数据库,然后调用Seed方法填充数据库。在后续的教程中我们可以看到,在我们部署程序后相同的过程会自动执行。
2.部署在Azure上:
本节可以选择性学习,我们可以跳过本节继续学习后续的教程,或者我们可以将程序发布在我们选择的其他托管服务商。
2.1.使用Code First迁移部署数据库:
当我们从Visual Studio创建用来配置部署设置的发布概要文件时,我们需要勾选Execute Code First Migrations (runs on application start)复选框。勾选后使得发布过程中会在目标服务器自动配置web.config文件,Code First使用 MigrateDatabaseToLatestVersion
初始化类。
在项目拷贝到目标服务器的过程中,Visual Studio不会在发布过程中对数据库进行任何操作。当我们运行部署后的程序时,它会在部署后第一次访问数据库。Code First会坚持数据库是否与数据模型匹配。如果不匹配,Code First会自动新建数据库(如果数据库不存在)或者修改数据库架构到最新版本(如果数据库存在但是与数据模型不匹配)。如果程序实现了迁移的Seed方法,该方法会在数据库新建或者架构修改后运行。
Seed方法插入测试数据。当我们部署程序到生产环境中时,我们必须修改Seed方法以保证只插入我们需要的数据。例如,在现在的数据模型中,我们可能需要真实的course数据而不是虚拟的student数据。我们可以编写Seed方法在开发环境中把这些数据都插入,但是在发布之前把插入student数据的代码注释掉。或者我们在Seed方法中只插入course数据,然后在UI中手动输入虚拟的student数据。
2.2.获取一个Azure帐号:
我们需要一个Azure帐号。如果没有Azure帐号,但是已经有一个MSDN订阅,那么可以activate your MSDN subscription benefits。不然,我们可以注册一个免费的试用帐号,但是它只能使用几分钟。查看更多详情:Azure Free Trial。
2.3.在Azure创建站点和数据库:
在Azure上的程序,运行在共享的主机环境,这意味着它运行在与其他Azure用户共享的虚拟机。共享主机环境是在云中的一种低成本的方式。以后如果我们的网站流量增加时,应用程序可以扩展以运行在专用的虚拟机。
我们将会把数据库部署在Azure SQL数据库。SQL数据库时一种建立在SQL Sever技术上的基于云的关系型数据库服务。适用于SQL Server的工具和程序同样也适用于SQL数据库。
2.3.1.在Azure Management Portal,在左边的选项卡点击Web Sites,然后点击New。
2.3.2.点击CUSTOM CREATE。New Web Site - Custom Create向导打开。
2.3.3.在New Web Site步骤,在URL框中输入一个字符串作为我们的程序的唯一URL。完整的URL将由我们的输入和旁边文本库的后缀组成。
2.3.4.在Region下拉框选择距离我们近的区域。该设置指定我们的程序运行在哪个数据中心。
2.3.5.在Database下拉框选择Create a free 20 MB SQL database。
2.3.6.在DB CONNECTION STRING NAME输入SchoolContext。
2.3.7.点击底部指向右边的箭头。向导将会进入Database Settings。
2.3.8.在Name框输入ContosoUniversityDB。
2.3.9.在Server框选择New SQL Database server。如果我们之前创建过服务器,我们可以从下拉框里选择。
2.3.10.输入管理员的LOGIN NAME和PASSWORD。如果我们选择New SQL Database server就意味着我们还没有用户名和密码,我们需要输入新的用户名和密码用来访问数据库。如果我们选择之前创建过的服务器,我们需要输入服务器的登录凭证。在本节教程中,我们不要选择Advanced复选框,该选项是用来设置数据库collation。
2.3.11.选择与之前我们在站点选择的相同的Region。
2.3.12.点击底部的检查标记,如果显示正确则表明我们创建完成。
Management Portal返回到站点页面,Status列显示站点正在创建。一段时间之后(通常少于一分钟)Status列显示站点被成功创建。在左边的导航栏,Web Sites图标旁边,将会显示我们的账户拥有的站点数量;SQL Databases图标旁边将会显示我们的账户拥有的数据库数量。
2.4.在Azure发布程序:
2.4.1.2.4.2.
2.4.3.如果我们之前没有在Visual Studio添加Azure订阅,则执行以下步骤。这些步骤让Visual Studio能够连接到我们的Azure订阅,这样 Import from Azure下拉框就能够包含我们的站点。
另外,我们可以直接使用Azure帐号登录而不用下载订阅文件。要使用这种方法,在下一步操作中我们点击 Sign In而不是 Manage subscriptions。这种方法更简单,但是本教程是2013年11月编写的,只有下载了订阅文件才能在Server Explorer中连接Azure SQL数据库。
2.4.3.1.在Import Publish Profile对话框,点击Manage subscriptions。
2.4.3.2.在Manage Azure Subscriptions对话框,点击Certificates标签,然后点击Import。
2.4.3.3.在Import Azure Subscriptions对话框点击Download subscription file。
2.4.3.4.在浏览器窗口,保存.publishsettings文件。
安全警告:publishsettings文件包含我们用于管理Azure订阅和服务的凭据(未加密)。比较安全的做法是把它暂时保存在源目录外面(例如,保存在Downloads文件夹),然后一旦导入完成就把它删除。一个恶意的用户从.publishsettings
文件获取到访问凭据,就可以编辑、新建或者删除我们的Azure服务。
2.4.3.5.在Import Azure Subscriptions对话框,点击Browse导航到.publishsettings文件。
2.4.3.6.点击Import。
2.4.4.关闭Manage Azure Subscriptions框。
2.4.5.在Import Publish Profile对话框,选择 Import from an Azure,从下拉框选择我们的站点,然后点击OK。
2.4.6.在Connection标签,点击Validate Connection以确保设置正确。
2.4.7.连接验证通过后,在Validate Connection按钮附近会显示一个绿色的复选框,点击Next。
2.4.8.点击SchoolContext下面的Remote connection string下拉框,选择我们创建的数据库的连接字符串。
2.4.9.勾选Execute Code First Migrations (runs on application start)。
这个设置使得发布过程中会在目标服务器自动配置web.config文件,Code First使用 MigrateDatabaseToLatestVersion
初始化类。
2.4.10.点击Next。
2.4.11.在Preview标签,点击Start Preview。
在这个标签显示了将会被拷贝到服务器的文件列表。显示预览不需要发布程序,但需要注意的是这是一个有用的功能。在这种情况下,我们不需要对文件列表做任何事情。等到我们下次发布时,只有修改了的文件才会被显示在该列表。
2.4.12.点击Publish。Visual Studio将会拷贝文件到Azure服务器。
2.4.13.Output窗口显示部署动作,并报告成功完成部署。
2.4.14.成功部署后,默认的浏览器将会自动打开部署站点的URL。我们点击Students标签将会看到:
此时我们的SchoolContext数据库已经被创建在Azure SQL数据库,因为我们选择了Execute Code First Migrations (runs on app start)。目标服务器的web.config文件已经被修改,因为 MigrateDatabaseToLatestVersion
初始化类将会在我们第一次读或者写数据库的时候运行(即在我们点击Students标签时)。
部署的过程中,同样产生了一个供Code First迁移更新数据库架构和添加测试数据的新的连接字符串:
我们可以在本机的ContosoUniversity\obj\Release\Package\PackageTmp\Web.config看到部署的Web.config文件版本。我们可以通过FTP配置文件本身访问Web.config文件,请查看ASP.NET Web Deployment using Visual Studio: Deploying a Code Update。
说明:现在这个程序没有考虑安全性,因为任何人都可以访问网站并修改数据。关于如何提高网站的安全性,请查看:Deploy a Secure ASP.NET MVC app with Membership, OAuth, and SQL Database to Azure。我们可以阻止其他人通过Azure Management Portal操作这个站点或者在Visual Studio的Server Explorer中停止站点。
2.5.高级迁移场景:
如果我们按照本教程的方法通过运行自动迁移部署一个数据库,并且部署一个运行在多个服务器上的网站,那么在同一时刻就会有多个服务器试图迁移。迁移时自动的,如果两个服务器同时运行相同的迁移,一个将会成功而另一个将会失败(假定操作不能被执行两次)。在这种情况下,如果我们想要避免这些问题,我们可以手动调用迁移并编写代码确保它只会运行一次。更多信息请查看:Running and Scripting Migrations from Code和Migrate.exe。
产看其他迁移场景的更多信息,请查看:Migrations Screencast Series。
2.6.Code First初始化:
在部署环节,我们看到使用了MigrateDatabaseToLatestVersion初始化。Code First同时提供了其他初始化,包括CreateDatabaseIfNotExists(默认的),DropCreateDatabaseIfModelChanges(我们之前使用的)和DropCreateDatabaseAlways。DropCreateAlways
初始化可以用于设置单元测试条件。我们也可以编写我们自己的初始化,我们也可以显示地调用初始化器如果我们不想一直等到应用程序读取或写入到数据库中。在本教程2013年11月编写的时候,在启用迁移之前我们只能使用新建和删除重建初始化。EF团队正致力于确保这些初始化正常迁移。
更多初始化信息,请查看:Understanding Database Initializers in Entity Framework Code First和Programming Entity Framework: Code First。