建立一个EF数据模型

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第一篇:建立一个EF数据模型

原文: Creating an Entity Framework Data Model

Contoso大学的Web应用程序

你在本教程中将建立一个简单的大学网站。

用户可以查看和更新学生信息,当然也包括教师的。下列图表是你将创建的应用程序截屏。

本网站的UI样式来源于内置的模板,所以教程可以将注意力集中在如何使用实体框架上。

创建一个MVC Web应用程序

打开VS并且创建一个新的C#Web项目,命名为ContosoUniversity。

在新建ASP.Net项目对话框中,选择MVC模板并点击更改身分验证按钮,选择无身份验证并确定,创建项目。

设置站点样式

我们将对站点目录,布局和主页面做些细微简单的改变。

打开 _Layout.cshtml并做以下更改:

  • 将"我的 ASP.NET 应用程序"及"应用程序名称"替换为"Contoso 大学"

  • 添加学生、教师、课程和部门的菜单项

完成后,你的代码应该和下列内容一致

 <!DOCTYPE html>
 <html>
 <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>@ViewBag.Title - Contoso 大学</title>
     @Styles.Render("~/Content/css")
     @Scripts.Render("~/bundles/modernizr")
 </head>
 <body>
     <div class="navbar navbar-inverse navbar-fixed-top">
         <div class="container">
             <div class="navbar-header">
                 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                     <span class="icon-bar"></span>
                     <span class="icon-bar"></span>
                     <span class="icon-bar"></span>
                 </button>
                 @Html.ActionLink("Contoso 大学", "Index", "Home", null, new { @class = "navbar-brand" })
             </div>
             <div class="navbar-collapse collapse">
                 <ul class="nav navbar-nav">
                     <li>@Html.ActionLink("主页", "Index", "Home")</li>
                     <li>@Html.ActionLink("关于", "About", "Home")</li>
                     <li>@Html.ActionLink("学生", "Index", "Student")</li>
                     <li>@Html.ActionLink("教师", "Index", "Instructor")</li>
                     <li>@Html.ActionLink("课程", "Index", "Course")</li>
                     <li>@Html.ActionLink("部门", "Index", "Department")</li>
                 </ul>
             </div>
         </div>
     </div>
     <div class="container body-content">
         @RenderBody()
         <hr />
         <footer>
             <p>&copy; @DateTime.Now.Year - Contoso 大学</p>
         </footer>
     </div>
 
     @Scripts.Render("~/bundles/jquery")
     @Scripts.Render("~/bundles/bootstrap")
     @RenderSection("scripts", required: false)
 </body>
 </html>

在View\Home\Index.cshtml中,用下面的代码替换原有的:

 @{
     ViewBag.Title = "Home Page";
 }
 
 <div class="jumbotron">
     <h1>Contoso 大学</h1>
 </div>
 
 <div class="row">
     <div class="col-md-4">
         <h2>欢迎访问Contoso大学</h2>
         <p>
            Contoso 大学是一个示例应用程序,演示了如何在MVC5中如何使用实体框架6来建立一个Web应用程序。
         </p>
     </div>
     <div class="col-md-4">
         <h2>从头建立</h2>
         <p>你可以跟随教程来一步步建立该应用程序。</p>
         <p><a class="btn btn-default" href="http://www.cnblogs.com/Bce-/p/3684643.html">查看教程 &raquo;</a></p>
     </div>
     <div class="col-md-4">
         <h2>直接下载</h2>
         <p>你可以从微软代码库中直接下载已完成的项目</p>
         <p><a class="btn btn-default" href="http://code.msdn.microsoft.com/ASPNET-MVC-Application-b01a9fe8">Download &raquo;</a></p>
     </div>
 </div>

按下Ctrl+F5运行网站,你可以看到主页及菜单。

安装实体框架6

在工具菜单中,点击NuGet程序包管理器,点击程序包管理器控制台。

在控制台中,输入以下命令并执行:

Install-Package EntityFramework

你可以看到实体框架包被添加到项目中。

创建数据模型

下面你将建立用于大学网站的实体类。你将从以下三个类开始:

注意Student和Enrollment存在一对多的关联关系。同样Course和Enrollment也是如此。换句话说,一个学生可以参加任意数量的课程,而一门课程也被任意数量的学生参加。

在下面的章节中我们将开始创建这些实体。

注意:如果你在这些实体类全部创建完成之前尝试运行项目,你将会得到编译器错误的提示。

学生实体

在Models文件夹下,创建Student类并使用以下的代码替换:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Web;
 
 namespace ContosoUniversity.Models
 {
     public class Student
     {
         public int ID { get; set; }
         public string LastName { get; set; }
         public string FirstMidName { get; set; }
         public DateTime EnrollmentDate { get; set; }
 
         public virtual ICollection<Enrollment> Enrollments { get; set; }
     }
 }

ID属性将成为数据表中对应该类的主键列。默认情况下,实体框架将命名为ID或类名+ID的属性用作主键列。

Enrollments属性是一个导航属性。导航属性建立本实体相关的其他实体之间的联系。在本例中,一个Student实体的Enrollments属性将容纳所有与Student实体相关联的Entollment实体。换句话说,如果一个Student在数据库中有两个相关的Enrollment行(列使用使用该学生的主键值作为外键),即Student实体的Enrollments属性将包含这两个关联的Enrollment实体。

导航属性通常被定义为virtual,使他们能获得某些实体框架的功能,比如延迟加载的优势。(关于延迟加载我们将在后面解释)

如果某个导航属性可以包含多个实体(如多对多或一对多关系),它的类型必须可以进行增删改操作,比如ICollection。

学生实体

在Models文件夹中,创建Enrollment类并用下面的代码替换现有的:

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Web;
 
 namespace ContosoUniversity.Models
 {
     public enum Grade
     {
         A, B, C, D, F
     }
 
 
     public class Enrollment
     {
         public int EnrollmentID { get; set; }
         public int CourseID { get; set; }
         public int StudentID { get; set; }
         public Grade? Grade { get; set; }
 
         public virtual Course Course { get; set; }
         public virtual Student Student { get; set; }
     }
 }

EnrollmentID属性将作为主键,它使用了类名+ID的模式而不是你在Student中直接使用ID本身的方法。通常情况下你应当在这两种方式中选择一种作为整个项目的统一命名方式,在这里我们只是演示了这两种方式的使用。在后面的教程中你将看到如何使用不带类名的ID从而更容易地在数据模型中实现继承。

Grade属性是一个枚举。属性后的问号表示这是一个可为空的属性。Null表示一个未知或没有分配的级别。

StudentID是一个外键,相应的导航属性是Student。一个Enrollment实体关联到一个Student实体。所以属性只能容纳一个Student实体(而不像之前的Student.Enrollments导航属性,它可以容纳多个Enrollments实体)。

同样CourseID也是一个外键,关联到一个Course实体。

如果一个属性的命名方式为导航属性名+主键属性名,实体框架便会将该属性视为外键属性。(例如,Student实体的主键为ID,则StudentID被视为为Student导航属性的外键)。外键的属性也可以命名为简单的主键属性名(例如,Course实体的主键为CourseID)。

课程实体

在Models文件夹中创建Course类并使用以下的代码替换模板代码:

 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
 using System.Web;
 
 namespace ContosoUniversity.Models
 {
     public class Course
     {
         [DatabaseGenerated(DatabaseGeneratedOption.None)]
         public int CourseID { get; set; }
         public string Title { get; set; }
         public int Credits { get; set; }
 
         public virtual ICollection<Enrollment> Enrollments { get; set; }
     }
 }

Enrollments属性是一个导航属性。一个Course实体可以和任意个Enrollment实体关联。

我们会在后面的教程中介绍更多的关于DatabaseGenerated特性。该特性可以让你来输入该实体的主键值,而不是让数据库自动生成它。

创建数据库上下文

在一个数据模型中负责协调实体框架功能的主类被称为数据库上下文类。您可以通过派生自System.Data.Entity.DbContext类来创建。你可以在代码中指定那些实体被包含在数据模型中。您可以可以自定义某些实体框架的行为。在本项目中,上下文类被命名为SchoolContext。

右键单击解决资源方案管理器中的项目,点击添加,点击新建文件夹。将新文件夹命名为DAL(数据访问层)。在该文件夹中创建一个SchoolContext类并使用以下代码替换模板代码:

 using ContosoUniversity.Models;
 using System;
 using System.Collections.Generic;
 using System.Data.Entity;
 using System.Data.Entity.ModelConfiguration.Conventions;
 using System.Linq;
 using System.Web;
 
 namespace ContosoUniversity.DAL
 {
     public class SchoolContext : DbContext
     {
         public SchoolContext() : base("SchoolContext") { }
 
         public DbSet<Student> Students { get; set; }
         public DbSet<Enrollment> Entollments { get; set; }
         public DbSet<Course> Courses { get; set; }
 
         protected override void OnModelCreating(DbModelBuilder modelBuilder)
         {
             modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
         }
     }
 }

指定实体集

这段代码为每个实体集合创建了一个DbSet属性。在实体框架中,一个实体集对应数据库中的表,一个实体对应数据表中的一行。

你可以省略DbSet<Enrollment>DbSet<Course>,实体框架将自动把它们包含进来。因为Student实体引用了Enrollment实体,并且Enrollment实体引用了Course实体。

指定连接字符串

连接字符串(稍后将被添加到web.config文件中)的名称被传递给构造函数。

 public SchoolContext() : base("SchoolContext")
 {
 }

你同样可以通过传递连接字符串而不是存储在web.config文件的连接字符串名称本身来指定连接。如果你不指定连接字符串或一个明确的名称,实体框架将假定连接字符串名称和类名称一致,即在本例中,默认的连接字符串名称为SchoolContext,同你显示声明的一致。

指定表名

OnModelCreating方法中的modelBuilder.Convertions.Remove被用来防止生成复数表名。如果你不这样做,在数据库中生成的数据表将被命名为Students,Courses及Entrollments。相反,在本例中我们的表名是Student,Course及Enrollment。对于表名称是否应该使用复数或单数命名模式并没有明确的要求。在本教程中我们将使用单数形式。重要的一点是,你可以选择任意的命名方式——通过是否注释掉该行代码。

设定初始化数据库并填充测试数据

当你运行程序时,实体框架可以自动创建(或自动删除并重新创建)数据表。你可以指定这应该在每次程序运行时进行或仅当模型发生了变化而不与现有的数据库同步时才进行。你也可以写一个Seed方法,以便在数据库初始化后自动填充测试数据到新的数据表中。

默认的行为是只有当该数据库不存在时才创建(当数据库已经存在时会抛出一个异常)。在本节中你将指定在每次模型发生变化时都删除旧数据库并建立一个新的。在本例中这样做是适当的。Seed方法将在重新创建后自动填充测试数据。而在生产中通常不希望这样做从而丢失数据库中的所有数据。稍后您将看到如何使用Code First Migration来改变数据库架构,而不是删除并重新创建。

在DAL文件夹中,创建一个SchoolInitializer类并使用以下代码替换默认的:

 using ContosoUniversity.Models;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Web;
 
 namespace ContosoUniversity.DAL
 {
     public class SchoolInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<SchoolContext>
     {
         protected override void Seed(SchoolContext context)
         {
             var students = new List<Student>
             {
             new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
             new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
             new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
             new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
             new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
             new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
             new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
             new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
             };
 
             students.ForEach(s => context.Students.Add(s));
             context.SaveChanges();
             var courses = new List<Course>
             {
             new Course{CourseID=1050,Title="Chemistry",Credits=,},
             new Course{CourseID=4022,Title="Microeconomics",Credits=,},
             new Course{CourseID=4041,Title="Macroeconomics",Credits=,},
             new Course{CourseID=1045,Title="Calculus",Credits=,},
             new Course{CourseID=3141,Title="Trigonometry",Credits=,},
             new Course{CourseID=2021,Title="Composition",Credits=,},
             new Course{CourseID=2042,Title="Literature",Credits=,}
             };
             courses.ForEach(s => context.Courses.Add(s));
             context.SaveChanges();
             var enrollments = new List<Enrollment>
             {
             new Enrollment{StudentID=,CourseID=1050,Grade=Grade.A},
             new Enrollment{StudentID=,CourseID=4022,Grade=Grade.C},
             new Enrollment{StudentID=,CourseID=4041,Grade=Grade.B},
             new Enrollment{StudentID=,CourseID=1045,Grade=Grade.B},
             new Enrollment{StudentID=,CourseID=3141,Grade=Grade.F},
             new Enrollment{StudentID=,CourseID=2021,Grade=Grade.F},
             new Enrollment{StudentID=,CourseID=1050},
             new Enrollment{StudentID=,CourseID=1050,},
             new Enrollment{StudentID=,CourseID=4022,Grade=Grade.F},
             new Enrollment{StudentID=,CourseID=4041,Grade=Grade.C},
             new Enrollment{StudentID=,CourseID=1045},
             new Enrollment{StudentID=,CourseID=3141,Grade=Grade.A},
             };
             enrollments.ForEach(s => context.Enrollments.Add(s));
             context.SaveChanges();
         }
     }
 }

Seed方法将数据库的上下文对象作为输入参数,并在方法中的代码使用该上下文对象将新的实体添加到数据库中。对于每个实体模型,代码创建新的实体集合添加他们到相应的DbSet属性,然后将更改保存到数据库。它并不是必须在每组实体后立即调用SaveChanges方法。但这样做有助于你在发生数据库写入异常时快速找到问题的根源。

向web.config中添加一个元素来告诉实体框架你将使用初始化类,比如下面的例子:

context 的Type属性指定了上下文的类名。你应当使用完整的类名和程序集名。同样databaseInitializer的Type指定了初始化类的名称。(如果你不想使用EF初始化,你可以在上下文元素中设置disableDatabaseInitialization="true")。

作为一种在web.config中设置初始值设定项的替代方法,你可以通过在Global.asax.cs中Application_Start方法中增加Database.SetInitializer语句来实现同样的功能。

现在应用程序已经设置为在程序首次运行时,对模型和数据库中的表进行比较,如果有区别,应用程序删除并重新创建该数据库。

注意:当你将应用程序部署到生产环境中时,你必须删除或禁用数据库重新创建代码,后面的教程会演示这一点。

使用SQL Server Express LocalDB数据库

LocalDB是SQL Server Express的一个轻量版本,非常适合用来进行本地测试,但不建议在生产中使用。

打开应用程序的web.config文件,添加数据库连接字符串,如下面的例子:

你添加的数据库连接字符串指定实体框架使用LocalDb作为数据库引擎,建立一个名为ContosoUniversity1.mdf的数据库(当前数据库还不存在,EF将自动创建它)。如果你想在你的App_data中存放数据库文件,您可以添加AttachDBFilename=|DataDirectory|\ContosoUniversity1.mdf到连接字符串。

创建Student控制器和视图

现在将创建一个网页来显示数据,并在请求数据的过程中自动触发数据库创建。你首先需要创建一个新的控制器,但在此之前,生成一次项目以将实体类提供给MVC控制器脚手架模型和上下文类。

右键点击Controllers文件夹,选择添加,单击新建搭建基架项。

在对话框中,选择包含视图的MVC 5控制器(使用实体框架)

在添加控制器对话框,使用下图的设置,然后添加。

当你点击添加时,基架将创建student控制器和一组视图(.cshtml文件)。在未来,当您使用实体框架创建项目时还可以获得一些附加功能:只需创建第一个模型类而无需创建连接字符串,然后在添加控制器时指定新的上下文类。该基架将创建数据库上下文类和连接字符串,以及控制器和视图。在VS中打开StudentController.cs文件,你将看到类中已经有一个实例化的数据库上下文对象:

private SchoolContext db = new SchoolContext();

Index方法从数据库上下文实例的Students属性读取Students实体集以获取学生列表

         public ActionResult Index()
         {
             return View(db.Students.ToList());
         }

Student\Index.cshtml视图将列表显示在表格中:

 @model IEnumerable<ContosoUniversity.Models.Student>
 
 @{
     ViewBag.Title = "Index";
 }
 
 <h2>Index</h2>
 
 <p>
     @Html.ActionLink("Create New", "Create")
 </p>
 <table class="table">
     <tr>
         <th>
             @Html.DisplayNameFor(model => model.LastName)
         </th>
         <th>
             @Html.DisplayNameFor(model => model.FirstMidName)
         </th>
         <th>
             @Html.DisplayNameFor(model => model.EnrollmentDate)
         </th>
         <th></th>
     </tr>
 
 @foreach (var item in Model) {
     <tr>
         <td>
             @Html.DisplayFor(modelItem => item.LastName)
         </td>
         <td>
             @Html.DisplayFor(modelItem => item.FirstMidName)
         </td>
         <td>
             @Html.DisplayFor(modelItem => item.EnrollmentDate)
         </td>
         <td>
             @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
             @Html.ActionLink("Details", "Details", new { id=item.ID }) |
             @Html.ActionLink("Delete", "Delete", new { id=item.ID })
         </td>
     </tr>
 }
 
 </table>

按下Ctrl+F5运行项目,点击学生选项卡已查看Seed方法插入的测试数据。

查看数据库

当你运行学生信息页面并且程序尝试存取数据时,实体框架会检查到没有已存在的数据库并尝试创建一个。然后运行Seed方法向数据库中填充数据。

你可以使用服务器资源管理器或SQL Server对象管理器来查看数据库,在本教程中,我们将使用服务器资源管理器。

  1. 关闭浏览器

  2. 在服务器资源管理器中,展开数据连接,展开School Context(ContosoUniversity),之后展开表,你会看到数据表已经建立,如下图:

  3. 右击Student表并点击显示表数据来查看表中的内容。

  4. 关闭服务器资源管理器。

ContosoUniversity1.mdf 和 .ldf 数据库文件通常存放在C:\User\你的用户名文件夹中。

由于你使用了DropCreateDatabaseIfModelChanges初始化器,你现在可以对Student类做一些改变,重新运行应用程序。数据库会自动重新建立数据表来匹配你所做出的改变。比如如果你添加了EmailAddress属性到Student类中,重新运行应用程序并打开Student页面,然后关闭页面,检查数据库表中的数据,你会看到新的EmailAddress列。

约定

因为使用了约定,你用于编写建立一个完整数据库的代码量已经降低到了最少。这些约定已经在之前的教程中被你使用到,或许你没有意识到你正在使用它们,包括:

  • 实体类型的复数形式被用作表名

  • 实体属性名被用作列名

  • 被命名为ID或实体名+ID的属性被用作主键。

  • 当一个属性以<导航属性名><主键属性名>时被用作外键(例如,Student实体的主键是ID,则StudentID为导航属性的外键)。你也可以使用简单的<主键属性名>(例如,Enrollment实体的主键是EnrollmentID,你可以直接使用EnrollmentID)。

你已经看到,约定可以被覆盖。例如指定表的名称不应当使用复数形式,你会看到以后如何明确标记属性作为外键属性。你将在后面的教程中了解更多有关约定及如何重写它们。

总结

现在,您已经创建了一个使用实体框架和SQL Server Express LocalDB来存储和显示数据的简单Web应用程序,在后面的教程中,您将学习如何执行基本的CRUD操作。

作者信息

Tom Dykstra - Tom Dykstra 是微软Web平台及工具团队的高级程序员,作家。