[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序使用高级功能

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第十二篇:为ASP.NET MVC应用程序使用高级功能html

原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Applicationios

译文版权全部,谢绝全文转载——但您能够在您的网站上添加到该教程的连接。程序员

在以前的教程中,您已经实现了继承。本教程引入了当你在使用实体框架Code First来开发ASP.NET web应用程序时能够利用的高级功能。web

对于本教程中所介绍的大多数主题,您将使用您已经建立的网页,使用原始的SQL进行批量更新。而后您将建立一个新的页面来更新数据库中全部课程的学分。sql

以及使用非跟踪的查询,你将在系编辑页面添加一个新的验证逻辑。数据库

执行原始的SQL查询

实体框架Code First API包含的方法使您能够直接发送SQL命令到数据库中。您有如下几种选择:api

  • 使用DbSet.SqlQuery方法来进行查询并返回实体类型。返回的对象类型必须是预期的DbSet对象,它们会由数据库上下文自动跟踪。除非您关闭跟踪。(参见下一节的AsNoTracking方法)
  • 使用Database.SqlQuery方法来进行查询并返回非实体类型。返回的对象不会被数据库上下文跟踪,即便您使用该方法来检索实体类型。
  • Database.ExecuteSqlCommand用于非查询类型的命令。

使用实体框架的优势之一是它可让你无需手工输入大量代码来实现存取数据的特定方法。经过自动生成SQL查询及命令,将你从繁琐的手工编码中解放出来。但在特殊状况下,您可能须要执行手工建立的特定的SQL查询,这些方法可以实现这一功能并为你提供异常处理。浏览器

当你常常性地在web应用程序中执行SQL命令时,你必须采起必要的预防措施来保护你的站点不受SQL注入攻击。其中的一个办法就是使用参数化的查询,确保来自web页的的字符串不会被解释为SQL命令。在本教程中,当您使用用户输入查询时,您将使用参数化的查询。缓存

调用一个查询来返回实体

DbSet<TEntity>类提供了一个方法,您可使用该方法来执行一个查询并返回一个实体类型。要观察该方法是如何工做的,你须要对Department控制器中的Details方法进行一些更改。mvc

在DepartmentController.cs中,使用下面的代码替换Details方法,高亮部分显示了须要进行的更改:

        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            //Department department = await db.Departments.FindAsync(id);
            string query = "select * from department where departmentid = @p0"; Department department = await db.Departments.SqlQuery(query, id).SingleOrDefaultAsync(); if (department == null)
            {
                return HttpNotFound();
            }
            return View(department);
        }

 

要验证新代码是否工做正常,请运行应用程序,转到系页面并点击某个系的详情。

你能够看到一切如以前同样正常工做。

调用一个查询来返回其余类型的对象

在较早的教程中您建立了一个学生统计网格用来显示每一个注册日期中注册的学生数目。这段代码使用了LINQ来进行操做:

            var data = from student in db.Students
                       group student by student.EnrollmentDate into dateGroup
                       select new EnrollmentDateGroup()
                       {
                           EnrollmentDate = dateGroup.Key,
                           StudentCount = dateGroup.Count()
                       };

假设您要直接编写SQL代码来进行该项查询而不是使用LINQ,您须要运行一个查询以返回实体类型之外的对象,这意味着您须要使用Database.SqlQuery方法。

在HomeController.cs中,使用下面的代码替换About方法,高亮部分显示了须要进行的更改:

        public ActionResult About()
        {
            //var data = from student in db.Students // group student by student.EnrollmentDate into dateGroup // select new EnrollmentDateGroup() // { // EnrollmentDate = dateGroup.Key, // StudentCount = dateGroup.Count() // };
            string query = "select EnrollmentDate,count(*) as studentCount "
                + "From Person "
                + "where discriminator = 'Student' "
                + "group by EnrollmentDate "; var data = db.Database.SqlQuery<EnrollmentDateGroup>(query); return View(data.ToList());
        }

 

 

运行页面,它会显示和以前同样的数据。

调用更新查询

假设管理员想要可以在数据库进行批量操做,例如为每一门课程更改学分。若是学校有大量的课程,针对每一门课程分别进行更新无疑是效率很是低下的作法。在本节中你会实现一个web页面使用户可以修改所有课程的学分,经过使用SQL Update语句来进行这一更改,以下图:

在CourseController.cs,添加HttpGet和HttpPost的UpdateCourseCredits方法:

        public ActionResult UpdateCourseCredits()
        {
            return View();
        }

        [HttpPost]
        public ActionResult UpdateCourseCredits(int ? multiplier)
        {
            if (multiplier != null)
            {
                ViewBag.RowsAffected = db.Database.ExecuteSqlCommand("update course Set Credits = Credits * {0}", multiplier);
            }
            return View();
        }

 

当控制器处理HttpGet请求时,ViewBag.RowsAffected将不返回任何值。视图将显示一个空的文本框及提交按钮。

当点击更新按钮时,调用HttpPost方法,获取在文本框中输入的值,代码执行SQL来更新课程并在ViewBag.RowsAffected中返回受影响的行数。当视图获取该变量的值,它将显示一条信息来讲明已经更新的课程数目,而不是文本框和提交按钮,以下图所示:

在CourseController.cs,右键点击UpdateCourseCredits方法,而后添加一个视图:

使用下面的代码替换视图中的:

@model ContosoUniversity.Models.Course
@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>UpdateCourseCredits</h2>

@if (ViewBag.RowsAffected == null)
{
    using (Html.BeginForm())
    {
        <p>
            Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
        </p>
        <p><input type="su" name="name" value="Update" /></p>
    }
}
@if (ViewBag.RowsAffected != null)
{
    <p>
        Number of rows updated: @ViewBag.RowsAffected
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

运行应用程序,添加"/UpdateCourseCredits"到浏览器地址栏中的末尾,如Http://localhost:40675/UpdateCourseCredits,打开页面,并在文本框中输入一个数字:

点击更新,你会看到受影响的课程:

而后返回列表,你会看到全部课程都进行了更新:

有关更多使用原始SQL查询的信息,请参阅MSDN上的Raw SQL Queries

非跟踪查询

当数据库上下文检索数据行并建立实体对象时,默认状况下它会跟踪内存中的实体是否与数据库中的同步。当您更新一个实体时,内存中的数据做为缓存。这种缓存在web应用程序中常常是不可用的,由于上下文实例一般是短生命期的(每一个请求都会建立一个新实例),而且上下文常常在读取过实体并使用后就将它们销毁了。

您可使用AsNoTracking方法来来禁用跟踪内存中的实体对象。在如下几种典型场景中,你可能须要这样作:

  • 须要检索大量的数据,而关闭跟踪可能会显著提升性能。
  • 您须要附加一个实体来更新它,但它是以前基于不一样的目的获取的同一个实体对象。由于该实体已经被数据库的上下文跟踪,你没法附加该实体以进行更改。这种状况下,你须要对较早的查询使用AsNoTracking选项。

在本节中你会实现上面第二个方案的业务逻辑。具体来讲,你会强制执行一名教师不能在多个系中担任主任的规则。

在DepartmentController.cs,添加一个新方法,使您能够从Edit和Create方法来调用它以确保没有两个系有相同的主任:

        private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
        {
            if (department.InstructorID != null)
            {
                Department duplicateDepartment = db.Departments
                    .Include("Administrator")
                    .Where(d => d.InstructorID == department.InstructorID)
                    .FirstOrDefault();
                if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
                {
                    string errorMessage = string.Format(
                        "教师{0} {1}已是{2}系的主任。",
                        duplicateDepartment.Administrator.FirstMidName,
                        duplicateDepartment.Administrator.LastName,
                        duplicateDepartment.Name);
                    ModelState.AddModelError(string.Empty, errorMessage);
                }
            }
        }

 

在HttpPost的Edit方法中的try代码块中调用该方法来验证,以下面的代码:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Edit([Bind(Include = "DepartmentID,Name,Budget,StartDate,RowVersion,InstructorID")] Department department)
        {
            try
            {
                if (ModelState.IsValid) { ValidateOneAdministratorAssignmentPerInstructor(department); } if (ModelState.IsValid)
                {
                    db.Entry(department).State = EntityState.Modified;
                    await db.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
            }
            catch (DbUpdateConcurrencyException ex)
            {
                var entry = ex.Entries.Single();
                var clientValues = (Department)entry.Entity;
                var databaseEntry = entry.GetDatabaseValues();
                if (databaseEntry == null)
                {
                    ModelState.AddModelError(string.Empty, "没法保存更改,系已经被其余用户删除。");
                }
                else
                {
                    var databaseValues = (Department)databaseEntry.ToObject();
                    if (databaseValues.Name != clientValues.Name)
                        ModelState.AddModelError("Name", "当前值: "
                            + databaseValues.Name);
                    if (databaseValues.Budget != clientValues.Budget)
                        ModelState.AddModelError("Budget", "当前值: "
                            + String.Format("{0:c}", databaseValues.Budget));
                    if (databaseValues.StartDate != clientValues.StartDate)
                        ModelState.AddModelError("StartDate", "当前值: "
                            + String.Format("{0:d}", databaseValues.StartDate));
                    if (databaseValues.InstructorID != clientValues.InstructorID)
                        ModelState.AddModelError("InstructorID", "当前值: "
                            + db.Instructors.Find(databaseValues.InstructorID).FullName);
                    ModelState.AddModelError(string.Empty, "当前记录已经被其余人更改。若是你仍然想要保存这些数据,"
                    + "从新点击保存按钮或者点击返回列表撤销本次操做。");
                    department.RowVersion = databaseValues.RowVersion;
                }
            }
            catch (RetryLimitExceededException)
            {
                ModelState.AddModelError(string.Empty, "没法保存更改,请重试或联系管理员。");
            }
            ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID);
            return View(department);
        }

 

运行系编辑页面,尝试将已是系主任的教师更改成另外一个系的主任,你会收到预期的错误消息:

再次运行系编辑页面,更改预算金额并保存,您会看到一个错误:

该错误的出现是如下缘由致使的:

  • 该Edit方法调用了ValidateOneAdministratorAssignmentPerInstructor方法,用来在所有系中检索系主任。这会致使要编辑的系被读取,因为此读取操做,该系实体正在被数据库上下文跟踪。
  • Edit方法尝试设置由模型绑定器建立的该实体的标志位为已修改的,并使用上下文隐式地尝试附加该实体。但上下文没法附加该实体,由于它已经被上下文跟踪了。

解决这一问题的一个办法是保持内存中用于验证查询的跟踪系实体的上下文,但这样作没有必要,由于你不须要更新该实体,或者从新从内存中读取它,但这样不会带来任何好处。

在验证方法中,指定不跟踪,以下面的代码所示:

                Department duplicateDepartment = db.Departments
                    .Include("Administrator")
                    .Where(d => d.InstructorID == department.InstructorID)
                    .AsNoTracking()
                    .FirstOrDefault();

 

重复以前的操做,这一次更新被成功保存。

 

检查发送到数据库的SQL

有时候,查看实际被发送到数据库的SQL查询是颇有帮助的,在较早的教程中,您看到了如何使用拦截器代码来执行这一工做,如今你将看到如何不使用拦截器的方法。要尝试该方法,你会检查一个简单查询并观察添加好比预先加载、过滤及排序,看看到底发生了什么。

在CourseController.cs,使用下面的代码替换原先的,以中止预先加载。

        public ActionResult Index()
        {
            var courses = db.Courses;
            var sql = courses.ToString();
            return View(courses.ToList());
        }

 

而后在return语句上设置一个断点,并按下F5在调试模式下运行该项目,选择课程索引页,当代码到达断点时,检查query变量,你将看到被发送的SQL的查询,它是一个简单的select语句。

你能够在监视窗口中使用文本可视化工具来检视SQL。

如今将一个下拉列表添加到课程索引页面,用户能够用来筛选特定的系。你会使用标题来进行排序,并指定系导航属性的预先加载。

在CourseController.cs,使用下面的代码替换Index方法:

        public ActionResult Index(int? SelectedDepartment)
        {
            var departments = db.Departments.OrderBy(q => q.Name).ToList();
            ViewBag.SelectedDepartment = new SelectList(departments, "DepartmentID", "Name", SelectedDepartment);
            int departmentID = SelectedDepartment.GetValueOrDefault();

            IQueryable<Course> courses = db.Courses
                .Where(c => !SelectedDepartment.HasValue || c.DepartmentID == departmentID)
                .OrderBy(d => d.CourseID)
                .Include(d => d.Department);
            var sql = courses.ToString();
            return View(courses.ToList());
        }

 

仍然在return上设置断点。

该方法接收下拉列表中选择的值,若是没有任何项目被选择,该参数为null。

一个包含全部系的SelectList集合被传递给视图的下拉列表。传递给SelectList的构造器的参数指定了值字段名,文本字段名和所选择的项目。

对于课程仓库的Get方法,代码指定了Department导航属性的筛选器表达式,一个排序和延迟加载。若是下拉下表中没有选择任何项,筛选表达式老是返回true。

在Views\Course\Index.cshtml中,在table开始标记以前,插入下面的代码来建立下拉列表和提交按钮。

@using (Html.BeginForm())
{
    <p>选择系:@Html.DropDownList("SelectedDepartment", "All")</p>
    <input type="submit" name="name" value="筛选" />
}

 

运行索引页,在一次遇到断点时继续运行以便显示页面,从下拉列表中选择一个系并点击筛选:

按照刚才的方法查看SQL语句,你会看到一个包含内链接查询的SQL。

SELECT 
    [Project1].[CourseID] AS [CourseID], 
    [Project1].[Title] AS [Title], 
    [Project1].[Credits] AS [Credits], 
    [Project1].[DepartmentID] AS [DepartmentID], 
    [Project1].[DepartmentID1] AS [DepartmentID1], 
    [Project1].[Name] AS [Name], 
    [Project1].[Budget] AS [Budget], 
    [Project1].[StartDate] AS [StartDate], 
    [Project1].[InstructorID] AS [InstructorID], 
    [Project1].[RowVersion] AS [RowVersion]
    FROM ( SELECT 
        [Extent1].[CourseID] AS [CourseID], 
        [Extent1].[Title] AS [Title], 
        [Extent1].[Credits] AS [Credits], 
        [Extent1].[DepartmentID] AS [DepartmentID], 
        [Extent2].[DepartmentID] AS [DepartmentID1], 
        [Extent2].[Name] AS [Name], 
        [Extent2].[Budget] AS [Budget], 
        [Extent2].[StartDate] AS [StartDate], 
        [Extent2].[InstructorID] AS [InstructorID], 
        [Extent2].[RowVersion] AS [RowVersion]
        FROM  [dbo].[Course] AS [Extent1]
        INNER JOIN [dbo].[Department] AS [Extent2] ON [Extent1].[DepartmentID] = [Extent2].[DepartmentID]
        WHERE @p__linq__0 IS NULL OR [Extent1].[DepartmentID] = @p__linq__1
    )  AS [Project1]
    ORDER BY [Project1].[CourseID] ASC

 

删除代码中的var sql = conrses.ToString();

仓储和单元工做模式

许多开发人员编写代码做为包装来实现实体框架的仓储和单元工做模式。这些模式在商业逻辑层和数据存取层之间建立了一个抽象层。实施这些模式能够帮助你的应用程序从数据存储的改变中隔离出来,而且促进自动化的单元测试开发。可是,对使用实体框架的程序编写额外的代码来实现这些模式并非最佳的选择,有如下几个缘由:

  • 实体框架上下文类自己就能够将你的代码从特定代码的数据存储中隔离。
  • 当你使用实体框架时,对于数据库更新操做实体框架上下文类能够做为一个工做单元类。
  • 在实体框架6版本中引入的功能使它在无需编写仓储代码的状况下来实现单元测试驱动。

有关如何执行仓储及单元工做模式的详细信息,请参阅the Entity Framework 5 version of this tutorial series。有关如何在实体框架6版本中执行单元测试驱动,请参阅:

代理类

在实体框架建立实体实例时(例如当你执行一个查询时),它老是建立做为动态生成的派生自实体的实体对象的代理。例以下面的两个调试器截图,在第一个图像中,您看到了一个预期为Student类型的student变量,在实例化实体后,第二个图像中你会看到该代理类。

代理类重写了实体的一些虚属性用来插入在访问属性时自动执行动做的钩子。其中一个使用这种机制就功能就是延迟加载。

大多数时候你并不会察觉到代理,但也有例外:

  • 某些状况下,你可能想要阻止实体框架建立代理实例。例如,一般你但愿对一个POCO类的实体进行序列化,而不是代理类。一种避免序列化问题的方法是序列化数据传输对象(DTOs)而不是实体对象,好比Using Web API with Entity Framework。另外一种方法就是disable proxy creation
  • 当你使用new运算符实例化一个实体类时,你获得的不是代理实例。这意味着你没法得到诸如延迟加载和自动跟踪的能力。这一般是好的:你通常不须要延迟加载,由于你须要建立一个并不在数据库中存在的新的实体,当你显式地将实体标记为Added时,你一般不须要修改跟踪。然而,若是你须要延迟加载,你须要更改跟踪,你能够经过使用DbSet类的Create方法经过代理来建立一个新实体对象。
  • 你可能会想要从一种代理类型得到一个真是的实体类型。ObjectContext类的GetObjectType方法能够用于得到代理类型的实际实体类型。

更多的信息,请参阅MSDN上的Working with Proxies

自动变化监测

实体框架使用比较实体的当前值和原始值来肯定一个实体是否被更改(以及所以而须要发送到数据库执行的更新)。实体在查询或附加时,原始值被保存起来。一些会致使自动变化监测的方法以下:

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.Savechanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

若是您正在跟踪大量实体,同时您在一个循环中调用了这些方法屡次,您可能会经过使用AutoDetectChangesEnabled属性来暂时关闭自动变化监测,从而得到程序性能的改进。

自动验证

当您调用SaveChanges方法时,在默认状况下,实体框架会在更新到数据库以前对全部已更改的实体中的所有属性进行验证。若是您更新了大量的实体而且已经对数据进行了验证,该工做是没必要要的,你能够经过暂时关闭验证来得到更少的处理保存时间。你可使用ValidateOnSaveEnabled属性。

Entity Framework Power Tools

Entity Framework Power Tools是一个简单的VS扩展,你可使用它来建立本教程中展现的数据模型图。该工具还能够作其余一些工做好比当你使用Code First时基于现有数据库的表来生成实体类。安装该工具后,你会在上下文菜单看到一些附加选项,例如,当你右键单击解决方案资源管理器的上下文类,你会获得一个选项来生成一个图表。当你使用Code First时没法修改关系图中的数据模型,但你能够移动图示使它更容易理解。

实体框架的源代码

你能够在http://entityframework.codeplex.com/得到实体框架6的源代码,除了源代码,你能够生成、跟踪问题、探查功能等更多,你能够提交bug并贡献你本身的加强功能给实体框架源代码。

虽然源代码是开放的,但实体框架是由微软彻底支持的产品。微软实体框架团队会不断地接收反馈及测试更改,以确保每一个版本的质量。

总结

这样,在ASP.NET MVC应用程序中使用实体框架这一系列教程就所有完成了。有关如何使用实体框架的更多信息,请参阅EF documentation page on MSDNASP.NET Data Access - Recommended Resources

有关如何在你创建应用程序后部署它,请参阅 ASP.NET Web Deployment - Recommended Resources

关于更多MVC的信息,请参阅 ASP.NET MVC - Recommended Resources

致谢

  • Tom Dykstra基于实体框架5编写了本教程的原始版本,并在之基础上编写了该教程。他是微软Web平台和工具团队的高级程序员做家。
  • Rick Anderson在实体框架5和MVC4教程中作了大量工做并合著了实体框架6更新,他是微软Azure和MVC的资深程序员做家。
  • Rowan Miller和其余的实体框架团队审查该教程并调试了大量的bug。

做者信息

 

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

相关文章
相关标签/搜索