EF6与MVC5系列(5):使用数据库迁移( Code First Migrations)和发布
本节教程包含以下内容:
- 启用数据库迁移(Code First Migrations):迁移特性可以改变数据模型,并且不需要删除重建数据库就可以修改数据库架构。
- 部署在Azure中:这步骤是可选的,可以不发布在Azure中继续学习本教程后面的内容。
ps:本文中只翻译Code First Migrations相关内容,有关如何在Azure中发布,可以查看原文:https://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/migrations-and-deployment-with-the-entity-framework-in-an-asp-net-mvc-application
启用Code First Migrations
在开发过程中,数据模型会频繁的变动,每次数据模型的修改都会导致数据模型与数据库不同步。我们已经配置了EF,这样在每次修改数据库模型以后EF会自动删除重建数据库。当你添加,移除,修改实体类或者修改DbContext
类,程序会自动删除已存在的数据库,创建与数据模型相匹配的数据库用于保存测试数据。
这种与数据库同步的方法在你发布网站之前是可以的,当实际运行中,程序中存储的都是我们想要的数据,而且我们不希望因为数据模型的变动导致这些数据丢失。 Code First Migrations特性通过启用 Code First 修改数据库架构解决了这个问题。而不需要删除重建数据库。这节中,我们将发布程序,并开始启用数据库迁移。
1.注释掉之前在Web.Config文件中添加的 contexts
节点。
<entityFramework> <!--<contexts> <context type="ContosoUniversity.DAL.SchoolContext,ContosoUniversity"> <databaseInitializer type="ContosoUniversity.DAL.SchoolInitailizer,ContosoUniversity" /> </context> </contexts>--> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="mssqllocaldb" /> </parameters> </defaultConnectionFactory>
2.同时修改连接字符串中的数据库名字为:ContosoUniversity2
<connectionStrings> <add name="SchoolContext" connectionString="Data Source=.;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" providerName="System.Data.SqlClient"/> <!--<add name="SchoolContext" connectionString="Data Source=(LocalDb)\mssqllocaldb;Initial Catalog=ContosoUniversity1;Integrated Security=SSPI;" providerName="System.Data.SqlClient"/>--> </connectionStrings>
这样第一次数据库迁移会创建一个新的数据库,不是必须这么做,但是这样比较好。
3.在vs中,工具->NuGet库程序包管理器->程序包管理器控制台
4.在控制台中键入以下命令:
enable-migrations
add-migration InitialCreate
enable-migrations命令在项目中创建了Migrations文件夹,并且在文件夹中有一个Configuration.cs文件可以用来配置迁移。
(如果在上述过程中,你省略了改变数据库名称这一步骤,数据库迁移会查找到已经存在的数据库,并自动添加add-migration命令,不过没关系,这样只不过会在你部署数据库之前不会运行测试的迁移代码。稍后运行update-database命令的时候,不会发生有任何变化,因为数据库已经存在了)
和你看到之前的初始化数据库类是一样的。Configuration类中包含一个Seed方法。这个方法是当你创建或者修改数据库之后可以插入或者修改数据。创建数据库以后会调用这个方法,每次数据模型改变导致数据库结构改变也会调用此方法。
修改Seed方法
删除重建数据库的时候使用初始化类的Seed方法添加测试数据,因为每次模型变动会导致数据库数据丢失。但使用了数据库迁移技术,原来的数据也会保留在数据库中,所以 在Seed方法中包含测试数据不是必须的。实际上真正项目部署的时候,我们并不希望Seed方法添加测试数据,而是添加我们需要的真实数据。
这节中,发布的时候我们将使用迁移,但是无论如何Seed方法都是添加测试数据,为了便于我们查看功能,使我们不需要手动添加大量数据。
1。修改Configuration.cs文件,下面代码将会在新的数据库中加载测试数据。
1 namespace ContosoUniversity.Migrations 2 { 3 using System; 4 using System.Data.Entity; 5 using System.Data.Entity.Migrations; 6 using System.Linq; 7 using ContosoUniversity.Models; 8 using System.Collections.Generic; 9 10 internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext> 11 { 12 public Configuration() 13 { 14 AutomaticMigrationsEnabled = false; 15 } 16 17 protected override void Seed(ContosoUniversity.DAL.SchoolContext context) 18 { 19 var students = new List<Student>{ 20 new Student { FirstMidName = "Carson", LastName = "Alexander", 21 EnrollmentDate = DateTime.Parse("2010-09-01") }, 22 new Student { FirstMidName = "Meredith", LastName = "Alonso", 23 EnrollmentDate = DateTime.Parse("2012-09-01") }, 24 new Student { FirstMidName = "Arturo", LastName = "Anand", 25 EnrollmentDate = DateTime.Parse("2013-09-01") }, 26 new Student { FirstMidName = "Gytis", LastName = "Barzdukas", 27 EnrollmentDate = DateTime.Parse("2012-09-01") }, 28 new Student { FirstMidName = "Yan", LastName = "Li", 29 EnrollmentDate = DateTime.Parse("2012-09-01") }, 30 new Student { FirstMidName = "Peggy", LastName = "Justice", 31 EnrollmentDate = DateTime.Parse("2011-09-01") }, 32 new Student { FirstMidName = "Laura", LastName = "Norman", 33 EnrollmentDate = DateTime.Parse("2013-09-01") }, 34 new Student { FirstMidName = "Nino", LastName = "Olivetto", 35 EnrollmentDate = DateTime.Parse("2005-08-11") } }; 36 students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s)); 37 context.SaveChanges(); 38 39 var courses = new List<Course> { 40 new Course {CourseID = 1050, Title = "Chemistry", Credits = 3, }, 41 new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3, }, 42 new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3, }, 43 new Course {CourseID = 1045, Title = "Calculus", Credits = 4, }, 44 new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4, }, 45 new Course {CourseID = 2021, Title = "Composition", Credits = 3, }, 46 new Course {CourseID = 2042, Title = "Literature", Credits = 4, } 47 }; 48 courses.ForEach(c=>context.Courses.AddOrUpdate(p=>p.Title,c)); 49 context.SaveChanges(); 50 51 var enrollments = new List<Enrollment>{ 52 new Enrollment { 53 StudentID = students.Single(s => s.LastName == "Alexander").ID, 54 CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 55 Grade = Grade.A 56 }, 57 new Enrollment { 58 StudentID = students.Single(s => s.LastName == "Alexander").ID, 59 CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 60 Grade = Grade.C 61 }, 62 new Enrollment { 63 StudentID = students.Single(s => s.LastName == "Alexander").ID, 64 CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 65 Grade = Grade.B 66 }, 67 new Enrollment { 68 StudentID = students.Single(s => s.LastName == "Alonso").ID, 69 CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 70 Grade = Grade.B 71 }, 72 new Enrollment { 73 StudentID = students.Single(s => s.LastName == "Alonso").ID, 74 CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 75 Grade = Grade.B 76 }, 77 new Enrollment { 78 StudentID = students.Single(s => s.LastName == "Alonso").ID, 79 CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 80 Grade = Grade.B 81 }, 82 new Enrollment { 83 StudentID = students.Single(s => s.LastName == "Anand").ID, 84 CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID 85 }, 86 new Enrollment { 87 StudentID = students.Single(s => s.LastName == "Anand").ID, 88 CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID, 89 Grade = Grade.B 90 }, 91 new Enrollment { 92 StudentID = students.Single(s => s.LastName == "Barzdukas").ID, 93 CourseID = courses.Single(c => c.Title == "Chemistry").CourseID, 94 Grade = Grade.B 95 }, 96 new Enrollment { 97 StudentID = students.Single(s => s.LastName == "Li").ID, 98 CourseID = courses.Single(c => c.Title == "Composition").CourseID, 99 Grade = Grade.B 100 }, 101 new Enrollment { 102 StudentID = students.Single(s => s.LastName == "Justice").ID, 103 CourseID = courses.Single(c => c.Title == "Literature").CourseID, 104 Grade = Grade.B 105 } 106 }; 107 foreach(Enrollment e in enrollments) 108 { 109 var enrollmentInDatabase = context.Enrollments.Where(s=> 110 s.Student.ID==e.StudentID && 111 s.Course.CourseID==e.CourseID).SingleOrDefault(); 112 if (enrollmentInDatabase ==null) 113 { 114 context.Enrollments.Add(e); 115 } 116 } 117 context.SaveChanges(); 118 } 119 } 120 }
Seed方法将context作为传入参数,并使用context在数据库中添加新数据。对于每种实体类型,创建新的实体集合,并将这些集合添加到相应的Dbset中,然后保存对数据库做的更改。没必要像上面代码如此,在每组实体后面都添加SaveChanges方法。此处这么做是为了当向数据库中添加实体的时候如果抛出异常便于我们找到问题源头。
添加数据的时候使用了AddOrUpdate方法。每次数据迁移之后调用执行 update-database
命令都会运行Seed方法,但这样不仅仅是插入数据,因为创建数据库第一次数据迁移以后有些数据已经存在。更新插入的操作可以防止当你向数据库中添加已存在的数据时报错,但它会覆盖所有数据修改。有时我们不希望这种事情发生:比如我们测试的时候修改了数据,但是我们仍希望数据库修改以后,这些数据仍然保留。这时候我们用条件插入操作:只有在数据库中不存在该记录的时候才添加数据。
AddOrUpdate 方法的第一个参数指定了使用某个属性去检查数据行是否存在。对于你正在提供的Student测试数据,LastName属性可以作为判断数据库中是否存在该数据的判断依据。因为列表中的每个LastName属性都是唯一的。
上述代码指定了LastName是唯一的,如果LastName不是唯一的。在下一次执行数据库迁移的时候会出现以下异常。
Sequence contains more than one element
对如何处理冗余数据,比如数据库中有两个名为:Alexander Carson的学生。请查看:Seeding and Debugging Entity Framework (EF) DBs 。有关更多AddOrUpdate
方法请查看: Take care with EF 4.3 AddOrUpdate Method
代码创建了Enrollment
实体,假设在student实体集中有ID值,尽管你并没有在创建集合的代码中设置这个属性。
new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, Grade = Grade.A },
在此处可以使用ID属性,是因为对Student调用了SaveChanges
方法的时候设置了ID属性。当将实体添加到数据库中的时候,EF会自动获取到主键值,并且修改内存中的ID属性。
将Enrollment
实体添加到Enrollments
实体集并没有使用AddOrUpdate方法。它检查了实体集中是否存在该实体,如果没有,就添加进去。这样我们通过运行程序创建新的入学信息到数据库,迁移的时候,创建的新信息将会保存在数据库。循环遍历Enrollment中的每个成员。如果数据库中不存在就添加该成员。第一次修改数据库的时数据库是空的,所以每一个enrollment都会添加进去。
foreach (Enrollment e in enrollments) { var enrollmentInDataBase = context.Enrollments.Where( s => s.Student.ID == e.Student.ID && s.Course.CourseID == e.Course.CourseID).SingleOrDefault(); if (enrollmentInDataBase == null) { context.Enrollments.Add(e); } }
2.生成项目
执行第一次迁移
当执行完 add-migration
命令时,会生成一个Migrations文件夹,里面包含一个<时间戳>_InitialCreate.cs文件。<时间戳>_InitialCreate.cs文件中的Up方法,会创建与数据模型实体相匹配的数据库,而里面的Down方法会删除数据库中的表。
迁移会调用UP方法实现数据模型的更改,当我们输入回滚修改的命令时,迁移会调用Down方法。
这些都是你输入add-migration InitialCreate
命令时候初始化迁移所创建的,命令中的InitialCreate
只是一个参数,它是任意命名的。
当数据库已经存在的时候你创建了初始化迁移,尽管数据库创建代码生成了但是它并不会运行,因为数据库已经和数据模型相匹配。当你部署到另外的没有数据库存在的环境中,代码才会运行并创建数据库。因此最好先测试下。这就是为什么先前修改连接字符串中的数据库名称。这样迁移可以创建一个新的数据库。
1.在程序包管理器控制台中输入以下命令:update-database
这个命令运行了InitialCreate中的up方法创建了数据库,运行Seed方法将数据填充到数据库中。当你部署网站的时候,也会发生同样的处理过程,稍后章节中将会看到。
2.在服务器资源管理器中查看你创建的数据库,如何查看已经在教程的第一章中讲到。运行程序看看是否和之前运行的一样。
使用数据库迁移部署数据库
原文请查看:https://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/migrations-and-deployment-with-the-entity-framework-in-an-asp-net-mvc-application