MVC程序中實體框架的Code First遷移和部署
這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,裏是第五篇:MVC程序中實體框架的Code First遷移和部署
原文: Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application
到目前爲止,應用程序已經可以在您本地機器上正常地運行。但如果您想將它發佈在互聯網上以便更多的人來使用,您需要將程序部署到WEB服務器。在本教程中你會將Contoso大學應用程序部署到Windows Azure網站的雲中。
本教程包含以下章節:
- 啓用Code First遷移。遷移功能能夠使您不必刪除並重新創建數據庫的數據架構而進行更改數據模型並部署你的更改到生產環境下。
- 將應用程序部署到Windows Azure。該步驟是可選的,你可以跳過本步驟繼續剩餘的教程。
我們建議使用源代碼管理的持續集成過程部署,但本教程並不包含那些主題。更多的信息請參見 source control 和 Building Real-World Cloud Apps with Windows Azure 。
啓用Code First遷移
當你進行新應用程序的開發時,你的數據模型會頻繁地變動。並且隨着每一次變動都會使數據模型與數據庫脫節。你已經成功配置了實體框架讓其在每一次你變更數據模型時自動刪除並重新創建數據庫。當您添加、刪除或更改實體類或者更改你的DbContext類時,重新運行應用程序會使它自動刪除已經存在的數據庫並創建一個和當前數據模型相匹配的數據庫。並且填充測試數據。
這種方法在保持數據模型和數據庫架構同步方面做得非常好,直到你準備將應用程序部署到生產環境。當應用程序開始生產並存儲生產數據,你當然不想因爲數據模型的變更而丟失成產數據(比如添加一個新列)。 Code First Migrations 功能解決了這個問題。通過啓用Code First遷移來更新數據庫架構,而不是刪除和重建數據庫。在本教程中,您會部署該應用程序,並準備啓用遷移。
禁用之前教程中你在Web.Config中設定的初始設定項。
<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="v11.0" />
</parameters>
</defaultConnectionFactory>
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
同樣在Web.config中,更改數據庫連接字符串的名爲成ContosoUniversity2。
<connectionStrings>
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ContosoUniversity2;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
此更改設置該項目的第一次遷移將創建一個新的數據庫,這不是必須的,但您稍後將看到爲這麼這樣做是一個不錯的主意。
從工具菜單上,單擊NuGet程序包管理器,單擊程序包管理器控制檯。
在控制檯中輸入以下命令:
enable-migrations
add-migration InitialCreate
enable-migrations命令將在項目中創建一個遷移文件夾。同時文件夾中包含一個Configuration.cs文件,你可以編輯該文件來配置遷移。 如果你在上一步中沒有更改數據庫名稱,遷移將找到現有的數據庫並自動執行add-migration命令,這沒有關係。它只是意味着你不會在部署數據庫之前運行遷移代碼的測試。之後當您運行update-database將不會做任何改變因爲數據庫已經存在。
如同之前教程中,Configuration類中同樣包含Seed方法。
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" }
// );
//
}
}
Seed方法的目的是使您在Code First創建或更新數據庫後插入或更新測試數據。當數據庫每次創建和更新數據庫架構時將調用該方法。
設置Seed方法
當您每次更改數據模型後,刪除和重新創建數據庫時你可以使用初始類的Seed方法來插入測試數據。因爲每次模型更改數據庫後,數據庫將被刪除,所有的測試數據都將丟失。在Code First前一種,測試數據在數據庫更改後是保留的。所以在Seed方法中包含測試數據通常不是必要的。事實上,你並不想要在使用遷移部署數據庫到生產環境時讓Seed方法來插入測試數據,因爲Seed方法會在生產環境中調用。在這種情況下,只有真正需要時,才使用Seed方法來在生產環境中插入數據。例如你可能想要在部署到生產環境時在Deparment表中包含實際部門的名稱。
對於本教程,您將使用遷移來部署。但爲了讓你能夠跟容易地看到程序功能是如何無需人工操作而插入數據的,我們將使用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;
}
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”操作。因爲你每次執行更新數據庫命令時Seed方法都會運行,通常在每個遷移後你不能只是插入數據。因爲您試圖添加的行有可能在創建數據庫後的第一次遷移中已經存在。”upsert”操作可以防止你試圖添加一個已經存在的行,但是它會重寫你在測試階段對數據進行的修改。你或許不希望這種情況在某些數據表中發生:在某些情況下你可能希望保留你在測試階段對測試數據所進行的更改。在這種情況下,你需要做一個條件插入操作:僅當它不存在時插入行。Seed方法同時使用以上兩種方法。
第一個傳遞給AddOrUpdate方法的參數,一個指定的屬性是用來檢查否行已經存在。對於您提供的測試學生數據,LastName屬性可以被用作檢查在每個列表中實體是否是唯一的。
context.Students.AddOrUpdate(p => p.LastName, s)
此代碼假定LastName是唯一的。如果您手動添加具有重複LastName的學生,你就會得到一個“序列包含多個元素”的異常。 有關如何處理容易數據,請參閱 Seeding and Debugging Entity Framework (EF) DBs 。有關AddOrUpdate方法的更多信息,請參閱 Take care with EF 4.3 AddOrUpdate Method 。 創建Enrollment實體的代碼假定你在學生集合中的實體已經擁有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值被設置。當實體被插入到數據庫時,實體框架會自動獲取該實體的主鍵值並且更新內存中實體上的ID屬性。 添加每個Enrollment實體到Enrollments實體集合的代碼不會使用AddOrUpdate方法,它會檢查每一個實體是否存在,如果不存在,則插入該實體。這種方法將保留通過使用應用程序UI對成績所做的修改。代碼遍歷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文件夾中,在文件名爲<時間戳>_InitalCreate.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方法。
這是您輸入add-migration InitialCreate命令時創建的初始遷移。參數(在該示例中是InitialCreate)用於文件的名稱,當然也可以是任意你想要的其他名稱。通常你會選擇一個單詞或短語來總結遷移中所做的改變。例如您可以能會命名之後的遷移爲”AddDeparmentTable”。
如果你創建了一個在數據庫已經存在的情況下的遷移,則生成的的數據庫創建代碼不會運行。因爲數據庫已經和數據模型匹配。將應用程序部署到另一個尚未創建數據庫的環境時,代碼纔會運行以創建數據庫。所以最好是提前測試一下。這就是爲什麼之前你更改了連接字符串中數據庫的名稱,以便遷移可以從零開始創建一個新的數據庫。
在程序包管理器控制檯中,輸入以下命令:
update-database
update-database命令運行Up方法來創建數據庫,然後運行Seed方法來填充數據庫。同樣的過程會在將程序部署到生產環境下發生,您將會在下一節看到。使用服務器資管管理器來檢查數據庫,驗證裏面的數據和程序同之前一樣運行正常。
部署到Windows Azure
因爲咱沒有Windows Azure的試用賬號,所以這部分翻譯就跳過了……反正也不影響學習的。
總結
在本節中你看到了如何使用Code First遷移,在下一節中你會開始進入高級主題,擴展數據模型。
作者信息
Tom Dykstra- Tom Dykstra 是微軟Web平臺及工具團隊的高級程序員,作家。