首页 > 代码库 > MVC5 Entity Framework学习之Code First迁移和部署

MVC5 Entity Framework学习之Code First迁移和部署

到目前为止,应用程序一直在本地IIS Express 上运行。为了让其他人能够通过互联网访问你的应用程序,你需要将它部署到WEB服务器。

本文章包含以下内容:

  • 启用Code First迁移,迁移功能能够让你不必重建数据库就可以更改数据模型并将其部署到生产环境。
  • 将应用程序部署到Windows Azure(可选)

1.启用Code First迁移

当你在开发应用程序时,你会对数据模型进行频繁的更改,随着每一次的更改,数据模型与数据库架构将不再一致。你已经对Entity Framework进行了配置让其在每一次数据模型更改时自动的删除并重新创建数据库。当你添加、删除或更改实体类或更改DbContext类后重新运行应用程序,它会自动的删除已存在的数据库并创建一个和当前数据模型相匹配的数据库,最后使用测试数据初始化。

在你将应用程序部署到生产环境之前,这种方法会一直保持数据模型和数据库架构的一致性。当应用程序运行在生产环境并已经存储了一部分数据时,你不想在数据模型变更(例如添加一个列)时丢掉这部分数据,Code First迁移功能能够通过更新数据库架构而不是删除并重建数据库来很好的解决这个问题。

1.禁用初始化程序,注释或删除Web.config文件中的contexts元素

<entityFramework>
  <!--<contexts>
    <context type="ContosoUniversity.DAL.SchoolContext, ContosoUniversity">
      <databaseInitializer type="ContosoUniversity.DAL.SchoolInitializer, ContosoUniversity" />
    </context>
  </contexts>-->
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
    <parameters>
      <parameter value=http://www.mamicode.com/"v11.0" />>2.同样在Web.config文件中,将数据库连接字符串中数据库的名字修改为ContosoUniversity2

<connectionStrings>
  <add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
上面的更改可以让应用程序在做第一次迁移时创建一个新的数据库,但这不是必须的。

3.依次打开Tools -> Library Package Manager -> Package Manager Console


4.在Package Manager Console中输入以下命令

enable-migrations
add-migration InitialCreate

enable-migrations命令会新建一个Migrations 文件夹,同时文件夹中包含一个Configuration.cs文件,你可以通过编辑该文件来配置迁移的相关设置。

如果你在上面的步骤中忘记更改数据库名称,迁移程序会找到现有的数据库并自动执行add-migration命令,不过这没什么关系,它只是意味着你在部署数据库之前不会对迁移代码进行测试,接下来当你执行update-database命令时,数据库架构不会发生任何变化因为数据库已经存在。


就像之前的初始化程序一样,Configuration类中包含有一个Seed方法

internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
        ContextKey = "ContosoUniversity.DAL.SchoolContext"
    }

    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" }
        //    );
        //
    }
}

Seed方法的目的是让你在Code First创建或更新数据库后可以插入或更新测试数据,当创建数据库或更新数据库架构时该方法被调用。

设置Seed方法

当你为每次数据模型的更改而删除和重新创建数据库时,你需要使用初始化类的Seed方法来插入测试数据,因为在每次数据模型更改后数据库都会被删除,同时所有的测试数据也会丢失。如果使用Code First迁移,测试数据是会被保留下来的,所以在Seed方法中包含测试数据通常不是必要的。事实上,如果在使用迁移功能将数据库部署至生产环境时,你不希望使用Seed方法来插入测试数据,而应该在你需要时再插入相应的测试数据。例如,当你将应用部署到生产环境时,你希望数据库Deparment表中应该包含department的相关数据。

在本文中,你会使用迁移功能来部署应用程序,但是为了能够更容易的观察应用程序是如何无需人工操作而自动插入数据的,我们会一直使用Seed方法来插入测试数据。

打开Configuration.cs,使用下面的代码替换;

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<ContosoUniversity.DAL.SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
            ContextKey = "ContosoUniversity.DAL.SchoolContext"
        }

        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();
        }
    }
}

Seed方法将数据库上下文对象作为输入参数,并使用该对象将实体添加到数据库,对于每一个实体类型,上面的代码创建了一个新的实体集合并将它们添加到适当的DbSet属性,最后将更改保存到数据库。在每一组实体之后调用SaveChanges方法并不是必需的,这里之所以这样做,是因为在将数据写入到数据库时如果发生异常,可以让你更容易的定位问题所在。

插入数据时使用AddOrUpdate方法来执行"upsert"操作,是因为在每次执行update-database命令时Seed方法总是会被调用,通常在每次迁移之后,你不仅仅是插入了数据,因为你试图添加的行可能在创建数据库后的第一次迁移中已经存在。"upsert"操作可以在你试图添加一个已存在的行时防止错误的发生,但是它会在你测试应用程序时重置你对数据所做的更改。你也许不希望发生下面的情况:在某些情况下你可能希望保留你在测试阶段对测试数据所做的更改,在另一种情况下,你希望做一个条件插入操作:当且仅当行不存在时才执行插入操作。Seed方法同时使用以上两种方法。

AddOrUpdate方法的第一个参数指定了一个属性来检查行是否已存在。对于提供的测试数据,LastName属性被用作检查列的唯一性

context.Students.AddOrUpdate(p => p.LastName, s)
上面的代码假定LastName具有唯一约束,如果你添加了一个与已有学生的LastName相同的学生,那么在执行迁移操作时会发生如下异常

Sequence contains more than one element
上面的代码在创建Enrollment实体时假定Students集合中的实体已经拥有ID值,即使在创建这些集合时没有为它们设置该值。

new Enrollment { 
    StudentID = students.Single(s => s.LastName == "Alexander").ID, 
    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
    Grade = Grade.A 
},

这里可以使用ID属性,因为当你为学生集合调用SaveChanges方法时ID值会被设置。EF在将实体保存至数据库时会自动获取该实体的主键值并将该值更新至内存中实体的ID属性。

将每个Enrollment实体添加到Enrollments实体集合的代码并没有使用AddOrUpdate方法,它会检查实体是否已存在,如果不存在,则插入该实体,这种方法将保存在应用程序界面对 enrollment grade所做的更改。该代码遍历Enrollment列表中的每个成员,如果在数据库中没有该Enrollment,就将该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);
    }
}
接下来请编译项目

执行第一次迁移

当执行add-migration命令时,迁移将生成用来创建数据库的代码,该代码位于Migrations文件夹中的名字为<timestamp>_InitialCreate.cs的文件中。InitialCreate类中的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方法。

当你输入add-migration InitialCreate命令时将创建一个初始化迁移,InitialCreate参数被用于文件名,当然你也可以修改为其它你想要的名称,通常该名称是一个单词或者短语,用来指明在迁移中要做的工作,例如,你可以将下一次迁移命名为"AddDeparmentTable"。

如果你创建了一个初始化迁移,但是数据库却已经存在,那么生成的用来创建数据库的代码是不会运行的,因为数据库架构和数据模型是一致的。当你将应用程序部署到另一个数据库尚不存在的环境中时,该代码将会运行以创建数据库,但最好是提前测试一下。这就是为什么要修改连接字符串中数据库的名称的原因所在,这样就可以创建一个全新的数据库。

在Package Manager Console窗口输入以下命令

update-database

************************************************************************************************************************

******************************以下内容仅供了解,大陆地区暂不能申请测试账号**********************************

************************************************************************************************************************

2.部署到Windows Azure

到目前为止应用程序一直运行在本地IIS Express,为了让其他人能够通过互联网访问你的应用程序,你需要将它部署到WEB服务器。接下来将教你如何将应用程序部署到Windows Azure。

使用Code First迁移来部署数据库

当你使用Visual Studio的配置设定来创建发布配置文件时,你需要勾选名为Execute Code First Migrations的复选框。此设置可以在部署过程中在目标服务器上自动配置应用程序的Web.config文件,以便Code First使用MigrateDatabaseToLatestVersion初始化类。

当部署程序将项目拷贝到目标服务器时,Visual Studio不需要做任何事情。当你运行已经部署的项目,且该项目第一次访问数据库时,Code First会检查数据库和数据模型是否一致。如果它们不一致,Code First会自动创建数据库(如果数据库不存在)或者更新数据库架构到最新版本(数据库存在但是模型不一致)。如果应用程序实现了迁移的Seed方法,那么该方法会在数据被创建或者架构被更新后执行。

Seed方法的作用是插入测试数据。如果你在将项目部署到生成环境,你必须修改Seed方法以便该方法仅在你想将数据插入到生产数据库时插入数据。举个例子来说,在当前的数据模型中,你可能希望在开发环境数据库中拥有真实的课程信息和虚构的学生信息,你可以在Seed方法中将这些信息添加至开发环境数据库,并在将项目部署到生产环境时注释掉添加虚构学生信息的那些代码。你也可以仅在Seed方法中添加课程信息,然后在应用程序界面中添加虚构的学生信息到测试数据库。

注册Windows Azure账号

你需要一个Windows Azure账号,如果你还没有但是却拥有一个MSDN订阅,你可以通过激活MSDN订阅来获得该账号。

在Windows Azure中新建一个网站和一个SQL数据库

Windows Azure网站运行在共享主机中,这意味着该网站运行在虚拟机中。共享主机环境是一种低成本的云端的环境,如果以后你的网站流量增加时,你可以将该网站运行在专用虚拟机上来满足需要。

你需要将数据库部署到Windows Azure SQL数据库中,SQL数据库是使用SQL Server技术建立的基于云的关系型数据库服务。运行在SQL Server中的工具盒应用同样也可以运行在SQL数据库中。

1.在Windows Azure Management Portal中,点击左侧的Web Sites选项卡,然后点击New


2.点击CUSTOM CREATE


打开New Web Site - Custom Create向导

3.在向导的New Web Site步骤,在URL文本框中输入作为应用程序URL的字符串,完整的URL应该包括你刚才输入的加上文本框右侧显示的字符串。下图中使用了"ConU"作为URL,但该URL可能已被别人使用,所以你必须填写一个未被使用过的名称作为URL。


4.在Region 下拉列表框中选择一个离你最近的选项,该设置会指定你的应用程序运行在哪个数据中心。

5.在Database下拉列表框中选择Create a free 20 MB SQL database选项。


6.在 DB CONNECTION STRING NAME中输入 SchoolContext


7.点击右下角的箭头图标,进入Database Settings步骤

8.在Name文本框中输入ContosoUniversityDB

9.在Server下拉框中选择New SQL Database server。另外,如果你之前创建了一个服务器,你可以从下拉框中选择你所创建的那个服务器。

10.输入管理员 LOGIN NAME 和PASSWORD,如果你选择的是New SQL Database server,这里输入的用户名和密码是过后访问数据库时的用户名和密码,如果你选择的是之前创建的服务器,你需要输入相应认证信息。

11.选择Region,要和之前选择的一致

12.点击右下角的检查图标,这表示你已经完成配置过程


完成之后会从Management Portal跳转至Web Sites页面,Status列会显示网站正在被创建,过一会之后(通常少于一分钟),Status列会显示网站已经被成功创建。

将应用程序部署到Windows Azure

1.打开Visual Studio,在Solution Explorer中右键点击项目,选择Publish


2.在 Publish Web向导的Profile选项卡中点击 Import


3.如果之前你没有在Visual Studio中添加Windows Azure订阅,请执行接下来的操作,这些操作可以让Visual Studio连接至Windows Azure订阅,以便Import from a Windows Azure web site下的下拉列表框中包含你所创建的网站。

作为另一种替代方法,你可以直接登录至 Windows Azure而不需要下载一个订阅文件,如果使用此方法,请在接下来的步骤中点击 Sign In 而不是Manage subscriptions。这种方法比较简单,但是本教程写于2013年9月,当时必须使用下载的订阅文件才能将Server Explorer连接至 Windows Azure SQL Database。

a.在 Import Publish Profile 对话框,点击 Manage subscriptions


b.在Manage Windows Azure Subscriptions对话框,点击Certificates 选项卡,然后点击Import.


c.在Import Windows Azure Subscriptions对话框,点击Download subscription file 


d.保存.publishsettings文件


安全注意事项:publishsettings文件中包含有你的凭证(未编码)以用来管理Windows Azure订阅和服务。对此文件来说最安全的做法就是先将其暂时存放在在某一个文件夹中,一旦导入完成就将其删除。如果被恶意用户访问到此文件,他就可以修改、新建或删除你的Windows Azure服务。

e.在 Import Windows Azure Subscriptions对话框,点击Browse并找到 .publishsettings文件


f.点击Import


4.关闭 Manage Windows Azure Subscriptions对话框

5.在Import Publish Profile对话框,选择 Import from a Windows Azure web site,从下拉列表框中选择你所创建的网站,点击OK


6. 在Connection选项卡,点击Validate Connection 来确保设置是正确的


7.当验证通过,Validate Connection旁边会出现一个绿色的对号标记,点击Next


8.打开SchoolContext 下面的Remote connection string下拉框,选择连接字符串

9.选择Execute Code First Migrations (runs on application start)


该设置会使部署程序在目的服务器上自动的配置应用程序的Web.config文件,以便Code First可以使用 MigrateDatabaseToLatestVersion初始化类

10.点击Next

11.在Preview 选项卡,点击Start Preview


该选项卡会显示将要被拷贝到目的服务器中的文件列表,当你再次部署该应用程序时,此时将会只显示那些被更改过的文件列表


12.点击Publish,Visual Studio会将文件拷贝至Windows Azure服务器

13.Output窗口会显示捕获到的部署动作和已成功完成的部署


14.部署成功之后,默认浏览器会自动打开该网站,该网站已成功的运行在云端,点击Students选项卡


此时SchoolContext 数据库已经在Windows Azure SQL Database中被成功创建,因为你选择了Execute Code First Migrations (runs on app start)选项。网站的Web.config文件已经被修改以便 MigrateDatabaseToLatestVersion初始化程序在第一次运行时读写数据库(当你点击Students选项卡时)。


在部署过程中同样也为Code First迁移创建了一个查询字符串(SchoolContext_DatabasePublish)来更新数据库架构并插入数据。


你可以在你本机的ContosoUniversity\obj\Release\Package\PackageTmp\Web.config中找到Web.config文件部署的版本。

注意:部署的web应用程序并不执行安全检查,所以任何知道此URL的人都可以修改数据库中的数据。你可以通过使用Windows Azure Management Portal 或者 Visual Studio中的 Server Explorer停止运行该网站以防止别人修改你的数据。


高级迁移方案

如果你通过运行迁移程序部署了一个数据库,那么接下来你将部署一个能运行在多个服务器上的网站,要做到这一点,你需要在多台服务器上同时运行迁移程序。迁移过程是原子的,如果在两台服务器上运行同一个迁移程序,则其中一个会成功,另一个会失败(假设操作不能被执行两次)。在这种情况下,如果你希望避免出现这个问题,你可以手动的调用迁移程序并修改你的代码以确保迁移只发生一次。

Code First初始化程序

在部署程序那一节,可以看到使用的是MigrateDatabaseToLatestVersion初始化程序,Code First同时也提供了其他的初始化程序,包括CreateDatabaseIfNotExists(默认)、DropCreateDatabaseIfModelChanges(之前使用的)和DropCreateDatabaseAlways。DropCreateAlways初始化程序对于为单元测试设置条件是非常有用的。你也可以编写你自己的初始化程序,如果你不想一直等直到应用程序能够读写数据库,你可以显示的调用初始化程序。

在部署部分中,您看到了正在使用的MigrateDatabaseToLatestVersion初始化。代码首先还提供了其它的初始化,包括(你之前使用)CreateDatabaseIfNotExists(默认),DropCreateDatabaseIfModelChanges和DropCreateDatabaseAlways。该DropCreateAlways初始化可以设立条件的单元测试非常有用。您也可以编写自己的初始化,您可以显式地调用一个初始化,如果你不想一直等到应用程序的读取或写入到数据库中。在本教程中被写入11月份2013的时候,你只能使用创建和DropCreate初始化启用迁移之前。实体框架团队正在努力使这些初始化实用与迁移为好。


THE END

MVC5 Entity Framework学习之Code First迁移和部署