MVC Web 应用程序的高级实体框架方案 (10)

作者 :Tom Dykstra

Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 创建 ASP.NET MVC 4 应用程序。 若要了解教程系列,请参阅本系列中的第一个教程

注意

如果遇到无法解决的问题, 请下载已完成的章节 ,并尝试重现问题。 通常可以通过将代码与已完成的代码进行比较来找到解决问题的解决方案。 有关一些常见错误以及如何解决这些问题,请参阅 错误和解决方法。

在上一教程中,你实现了工作模式的存储库和单元。 本教程涵盖以下主题:

  • 执行原始 SQL 查询。
  • 执行无跟踪查询。
  • 检查发送到数据库的查询。
  • 使用代理类。
  • 禁用自动检测更改。
  • 保存更改时禁用验证。
  • 错误和解决方法

对于其中大多数主题,你将使用已创建的页面。 若要使用原始 SQL 执行批量更新,你将创建一个新页面,用于更新数据库中所有课程的信用额度数:

显示“更新课程额度”初始页的屏幕截图。数字 2 在文本字段中输入。

若要使用无跟踪查询,需向“部门编辑”页添加新的验证逻辑:

显示 Contoso 大学系编辑页面的屏幕截图,其中包含重复的管理员错误消息。

执行原始 SQL 查询

Entity Framework Code First API 包含用于将 SQL 命令直接传递到数据库的方法。 你有以下选择:

  • 使用DbSet.SqlQuery返回实体类型的查询方法。 返回的对象必须是该对象预期的 DbSet 类型,除非关闭跟踪,否则数据库上下文会自动跟踪这些对象。 (请参阅以下有关 AsNoTracking method.) 的部分
  • Database.SqlQuery 返回非实体类型的查询使用此方法。 数据库上下文不会跟踪返回的数据,即使你使用该方法来检索实体类型也是如此。
  • 对非查询命令使用 Database.ExecuteSqlCommand

使用 Entity Framework 的优点之一是它可避免你编写跟数据库过于耦合的代码 它会自动生成 SQL 查询和命令,使得你无需自行编写。 但是,当你需要运行手动创建的特定 SQL 查询时,有一些特殊情况,并且这些方法使你能够处理此类异常。

在 Web 应用程序中执行 SQL 命令时,请务必采取预防措施来保护站点免受 SQL 注入攻击。 一种方法是使用参数化查询,确保不会将网页提交的字符串视为 SQL 命令。 在本教程中,将用户输入集成到查询中时会使用参数化查询。

调用返回实体的查询

假设希望 GenericRepository 该类提供额外的筛选和排序灵活性,而无需使用其他方法创建派生类。 实现此目的的一种方法是添加接受 SQL 查询的方法。 然后,可以指定控制器中所需的任何类型的筛选或排序,例如 Where 依赖于联接或子查询的子句。 在本部分中,你将了解如何实现此类方法。

GetWithRawSql通过将以下代码添加到 GenericRepository.cs 来创建方法:

public virtual IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters)
{
    return dbSet.SqlQuery(query, parameters).ToList();
}

CourseController.csDetails ,从该方法调用新方法,如以下示例所示:

public ActionResult Details(int id)
{
    var query = "SELECT * FROM Course WHERE CourseID = @p0";
    return View(unitOfWork.CourseRepository.GetWithRawSql(query, id).Single());
}

在这种情况下,可以使用 GetByID 该方法,但你正在使用 GetWithRawSql 该方法来验证 GetWithRawSQL 该方法是否有效。

运行“详细信息”页,验证选择查询是否正常工作, (选择“ 课程 ”选项卡,然后选择一个课程) 的详细信息

显示 Contoso 大学详细信息页的屏幕截图。

调用返回其他类型的对象的查询

之前你在“关于”页面创建了一个学生统计信息网格,显示每个注册日期的学生数量。 HomeController.cs 中执行此操作的代码使用 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.csAbout ,将方法中的 LINQ 语句替换为以下代码:

var query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
    + "FROM Person "
    + "WHERE EnrollmentDate IS NOT NULL "
    + "GROUP BY EnrollmentDate";
var data = db.Database.SqlQuery<EnrollmentDateGroup>(query);

运行“关于”页。 显示的数据和之前一样。

显示 Contoso University About 页面的屏幕截图。

调用更新查询

假设 Contoso 大学管理员希望能够在数据库中执行批量更改,例如更改每个课程的学分数。 如果该大学提供了大量课程,那么将所有课程作为实体来检索并单独更改就非常低效。 在本部分中,你将实现一个网页,该网页允许用户指定一个因素来更改所有课程的学分数,并通过执行 SQL UPDATE 语句进行更改。 网页的外观类似于下图:

显示“更新课程额度”初始页的屏幕截图。数字 2 在文本字段中输入。

在上一教程中,你使用泛型存储库读取和更新 Course 控制器中的 Course 实体。 对于此批量更新操作,需要创建新的不在泛型存储库中的存储库方法。 为此,你将创建一个派生自该类的GenericRepository专用CourseRepository类。

DAL 文件夹中,创建 CourseRepository.cs ,并将现有代码替换为以下代码:

using System;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class CourseRepository : GenericRepository<Course>
    {
        public CourseRepository(SchoolContext context)
            : base(context)
        {
        }

        public int UpdateCourseCredits(int multiplier)
        {
            return context.Database.ExecuteSqlCommand("UPDATE Course SET Credits = Credits * {0}", multiplier);
        }

    }
}

UnitOfWork.cs 中,将Course存储库类型从更改为GenericRepository<Course>CourseRepository:

private CourseRepository courseRepository;
public CourseRepository CourseRepository
{
    get
    {

        if (this.courseRepository == null)
        {
            this.courseRepository = new CourseRepository(context);
        }
        return courseRepository;
    }
}

CourseController.cs 中,添加方法 UpdateCourseCredits

public ActionResult UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewBag.RowsAffected = unitOfWork.CourseRepository.UpdateCourseCredits(multiplier.Value);
    }
    return View();
}

此方法将用于这两种方法 HttpGetHttpPostHttpGetUpdateCourseCredits方法运行时,multiplier变量将为 null,视图将显示空文本框和提交按钮,如上图所示。

单击“ 更新 ”按钮并 HttpPost 运行该方法时, multiplier 将在文本框中输入该值。 然后,代码调用存储库 UpdateCourseCredits 方法,该方法返回受影响的行数,该值存储在对象中 ViewBag 。 当视图接收对象中 ViewBag 受影响的行数时,它会显示该数字,而不是文本框和提交按钮,如下图所示:

显示 Contoso University Update Course Credits 行受影响的页面的屏幕截图。

在“更新课程额度”页的 “视图\课程 ”文件夹中创建视图:

显示“添加视图”对话框的屏幕截图。更新课程额度在“视图名称”文本字段中输入。

Views\Course\UpdateCourseCredits.cshtml 中,将模板代码替换为以下代码:

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</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="submit" value="Update" />
        </p>
    }
}
@if (ViewBag.RowsAffected != null)
{
    <p>
        Number of rows updated: @ViewBag.RowsAffected
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

通过选择Courses选项卡运行UpdateCourseCredits方法,然后在浏览器地址栏中 URL 的末尾添加"/ UpdateCourseCredits"到 (例如: http://localhost:50205/Course/UpdateCourseCredits)。 在文本框中输入数字:

显示“更新课程信用额度”初始页的屏幕截图,其中输入了文本字段中的编号 2。

单击Update。 你会看到受影响的行数:

显示更新了行数的“更新课程额度”页的屏幕截图。

单击“返回列表”可以查看课程列表,其中学分已替换为修改后的数字。

显示“课程索引”页的屏幕截图。课程列表显示了修订后的信用额度。

有关原始 SQL 查询的详细信息,请参阅 Entity Framework 团队博客上的 原始 SQL 查询

非跟踪查询

当数据库上下文检索数据库行并创建表示它们的实体对象时,默认情况下,它会跟踪内存中的实体是否与数据库中的内容同步。 更新实体时,内存中的数据充当缓存并使用该数据。 在 Web 应用程序中,此缓存通常是不必要的,因为上下文实例通常生存期较短(创建新的实例并用于处理每个请求),并且通常在再次使用该实体之前处理读取实体的上下文。

可以使用该方法指定上下文是否跟踪查询 AsNoTracking 的实体对象。 可能想要执行的典型方案包括以下操作:

  • 查询检索如此大量的数据,关闭跟踪可能会明显提高性能。
  • 要附加实体以更新该实体,但之前出于其他目的检索了同一实体。 由于数据库上下文已跟踪了该实体,因此无法附加要更改的实体。 防止这种情况发生的方法之一是将 AsNoTracking 选项与前面的查询一起使用。

在本部分中,你将实现说明这些方案的第二个业务逻辑。 具体而言,你将强制实施业务规则,该规则显示讲师不能是多个部门的管理员。

DepartmentController.cs 中,添加一个新方法,可以从和Create方法调用Edit,以确保没有两个部门具有相同的管理员:

private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
    if (department.PersonID != null)
    {
        var duplicateDepartment = db.Departments
            .Include("Administrator")
            .Where(d => d.PersonID == department.PersonID)
            .FirstOrDefault();
        if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
        {
            var errorMessage = String.Format(
                "Instructor {0} {1} is already administrator of the {2} department.",
                duplicateDepartment.Administrator.FirstMidName,
                duplicateDepartment.Administrator.LastName,
                duplicateDepartment.Name);
            ModelState.AddModelError(string.Empty, errorMessage);
        }
    }
}

如果不存在验证错误,请在HttpPostEdit方法块中添加try代码以调用此新方法。 块 try 现在类似于以下示例:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, PersonID")]
    Department department)
{
   try
   {
      if (ModelState.IsValid)
      {
         ValidateOneAdministratorAssignmentPerInstructor(department);
      }

      if (ModelState.IsValid)
      {
         db.Entry(department).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DbUpdateConcurrencyException ex)
   {
      var entry = ex.Entries.Single();
      var clientValues = (Department)entry.Entity;

运行“部门编辑”页,并尝试将部门管理员更改为已是其他部门的管理员的讲师。 收到预期的错误消息:

显示“部门编辑”页的屏幕截图,其中包含重复的管理员错误消息。

现在再次运行“部门编辑”页,这次更改 预算 金额。 单击“ 保存”时,会看到错误页:

显示带有对象状态管理器错误消息的“部门编辑”页的屏幕截图。

异常错误消息为“An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.”这是由于以下事件序列而发生的:

  • 该方法 Edit 调用该方法 ValidateOneAdministratorAssignmentPerInstructor ,该方法检索所有将 Kim Abercrombie 作为其管理员的部门。 这导致英语系被阅读。 因为这是正在编辑的部门,因此不会报告任何错误。 但是,由于此读取操作,从数据库读取的英语部门实体现在正由数据库上下文跟踪。
  • 该方法 Edit 尝试在 MVC 模型绑定器创建的英语部门实体上设置 Modified 标志,但这失败,因为上下文已经跟踪英语部门的实体。

此问题的一个解决方法是使上下文无法跟踪验证查询检索到的内存中部门实体。 这样做没有缺点,因为不会更新此实体或再次读取它的方式,从而受益于它在内存中缓存。

DepartmentController.cs 中,在 ValidateOneAdministratorAssignmentPerInstructor 方法中,指定无跟踪,如下所示:

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

重复尝试编辑部门 的预算 金额。 这次操作成功,网站将按预期返回“部门索引”页,其中显示了修订的预算值。

检查发送到数据库的查询

有时能够以查看发送到数据库的实际 SQL 查询对于开发者来说是很有用的。 为此,可以在调试器中检查查询变量,或调用查询 ToString 的方法。 若要尝试此操作,你将查看一个简单的查询,然后在添加诸如急切加载、筛选和排序等选项时查看它会发生什么情况。

Controllers/CourseController 中,将 Index 方法替换为以下代码:

public ViewResult Index()
{
    var courses = unitOfWork.CourseRepository.Get();
    return View(courses.ToList());
}

现在,在 GenericRepository.csreturn query.ToList(); 设置方法的语句和 return orderBy(query).ToList(); 语句中的 Get 断点。 在调试模式下运行项目,然后选择“课程索引”页。 当代码到达断点时,检查 query 变量。 会看到发送到SQL Server的查询。 这是一个简单的 Select 语句:

{SELECT 
[Extent1].[CourseID] AS [CourseID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Credits] AS [Credits], 
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}

显示示例 Web 应用程序“通用存储库”选项卡的屏幕截图。选择查询变量。

查询可能太长,无法在 Visual Studio 的调试窗口中显示。 若要查看整个查询,可以复制变量值并将其粘贴到文本编辑器中:

显示变量值的屏幕截图,其中选择了下拉菜单时显示的变量值。突出显示了“复制值”选项。

现在,你将将下拉列表添加到“课程索引”页,以便用户可以筛选特定部门。 你将按标题对课程进行排序,并指定导航属性的 Department 预先加载。 在 CourseController.cs 中,将 Index 该方法替换为以下代码:

public ActionResult Index(int? SelectedDepartment)
{
    var departments = unitOfWork.DepartmentRepository.Get(
        orderBy: q => q.OrderBy(d => d.Name));
    ViewBag.SelectedDepartment = new SelectList(departments, "DepartmentID", "Name", SelectedDepartment);

    int departmentID = SelectedDepartment.GetValueOrDefault(); 
    return View(unitOfWork.CourseRepository.Get(
        filter: d => !SelectedDepartment.HasValue || d.DepartmentID == departmentID,
        orderBy: q => q.OrderBy(d => d.CourseID),
        includeProperties: "Department"));
}

该方法在参数中 SelectedDepartment 接收下拉列表的选定值。 如果未选择任何内容,此参数将为 null。

SelectList包含所有部门的集合将传递给下拉列表的视图。 传递给 SelectList 构造函数的参数指定值字段名称、文本字段名称和所选项。

Get对于存储库的方法Course,代码指定筛选器表达式、排序顺序和导航属性的Department预先加载。 如果下拉列表 (中选择任何内容,SelectedDepartment则筛选器表达式始终返回 true null) 。

Views\Course\Index.cshtml 中,紧接在打开 table 标记之前,添加以下代码以创建下拉列表和提交按钮:

@using (Html.BeginForm())
{
    <p>Select Department: @Html.DropDownList("SelectedDepartment","All")   
    <input type="submit" value="Filter" /></p>
}

在类中 GenericRepository 仍设置断点后,运行“课程索引”页。 继续执行代码命中断点的前两次,以便页面显示在浏览器中。 从下拉列表中选择部门,然后单击“ 筛选器

显示“课程索引”页的屏幕截图,其中选择了“经济学系”。

这一次,第一个断点将用于部门查询下拉列表。 在下次代码到达断点时跳过该变量并查看 query 变量,以查看 Course 查询现在的外观。 你将看到如下所示的内容:

{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].[PersonID] AS [PersonID], 
[Extent2].[Timestamp] AS [Timestamp]
FROM  [Course] AS [Extent1]
INNER JOIN [Department] AS [Extent2] ON [Extent1].[DepartmentID] = [Extent2].[DepartmentID]
WHERE (@p__linq__0 IS NULL) OR ([Extent1].[DepartmentID] = @p__linq__1)}

可以看到,查询现在是一个JOIN查询,该查询会随数据一起Course加载Department数据,并且它包含子WHERE句。

使用代理类

例如,当 Entity Framework 创建实体实例 (时,执行查询) 时,它通常会将其创建为动态生成的派生类型的实例,充当实体的代理。 此代理替代实体的一些虚拟属性,以插入挂钩,用于在属性被访问时自动执行操作。 例如,此机制用于支持延迟加载关系。

大多数时候,你不需要注意这种代理的使用,但有例外情况:

  • 在某些情况下,你可能希望阻止实体框架创建代理实例。 例如,序列化非代理实例比序列化代理实例更有效。
  • 使用 new 运算符实例化实体类时,不会获取代理实例。 这意味着你不会获得延迟加载和自动更改跟踪等功能。 这通常没问题;通常不需要延迟加载,因为你正在创建不在数据库中的新实体,并且如果显式将实体 Added标记为更改跟踪,则通常不需要更改跟踪。 但是,如果你确实需要延迟加载,并且需要更改跟踪,则可以使用 Create 类的方法 DbSet 使用代理创建新的实体实例。
  • 可能需要从代理类型获取实际实体类型。 可以使用 GetObjectType 类的方法 ObjectContext 获取代理类型实例的实际实体类型。

有关详细信息,请参阅 Entity Framework 团队博客上的 “使用代理 ”。

禁用自动检测更改

Entity Framework 通过比较的实体的当前值与原始值来判断更改实体的方式 (因此需要发送更新到数据库)。 查询或附加实体时,将存储原始值。 如下方法会导致自动脏值:

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

如果要跟踪大量实体,并在循环中多次调用其中一种方法,则可以通过使用 AutoDetectChangesEnabled 属性暂时关闭自动更改检测,从而获得显著的性能改进。 有关详细信息,请参阅 自动检测更改

保存更改时禁用验证

调用 SaveChanges 该方法时,默认情况下,Entity Framework 会在更新数据库之前验证所有已更改实体的所有属性中的数据。 如果已更新大量实体并已验证数据,则此操作是不必要的,可以通过暂时关闭验证来缩短保存更改的过程。 可以使用 ValidateOnSaveEnabled 属性执行此操作。 有关详细信息,请参阅 验证

摘要

这将完成本系列教程,介绍如何在 ASP.NET MVC 应用程序中使用 Entity Framework。 可以在 ASP.NET 数据访问内容映射中找到指向其他 Entity Framework 资源的链接。

有关如何在生成 Web 应用程序后部署 Web 应用程序的详细信息,请参阅 MSDN 库中 ASP.NET 部署内容映射

有关与 MVC 相关的其他主题(如身份验证和授权)的信息,请参阅 MVC 建议的资源

致谢

  • Tom Dykstra 编写了本教程的原始版本,是Microsoft Web 平台和工具内容团队的高级编程作家。
  • 里克·安德森 (推特 @RickAndMSFT) 共同创作本教程,并做了大部分工作更新 EF 5 和 MVC 4。 Rick 是面向 Microsoft 的高级编程作家,专注于 Azure 和 MVC。
  • Entity Framework 团队的 Rowan Miller 和其他成员协助进行代码评审,并帮助调试在更新 EF 5 教程时出现的许多迁移问题。

VB

最初生成本教程时,我们同时提供了已完成下载项目的 C# 和 VB 版本。 通过此更新,我们将为每个章节提供一个 C# 可下载项目,以便更轻松地在系列中的任何位置开始,但由于时间限制和其他优先级,我们没有这样做 VB。 如果使用这些教程生成 VB 项目,并且愿意与他人共享该项目,请告知我们。

错误和解决方法

无法创建/卷影复制

错误消息:

当该文件已存在时,无法创建/卷影复制“DotNetOpenAuth.OpenId”。

解决方案:

等待几秒钟并刷新页面。

无法识别Update-Database

错误消息:

术语“Update-Database”无法识别为 cmdlet、函数、脚本文件或可操作程序的名称。 检查名称的拼写,或者是否包含路径,请验证路径是否正确,然后重试。 (Update-Database PMC.) 中的命令

解决方案:

退出 Visual Studio。 重新打开项目,然后重试。

验证失败

错误消息:

一个或多个实体的验证失败。 有关详细信息,请参阅“EntityValidationErrors”属性。Update-Database PMC.) 中的命令 (

解决方案:

此问题的一个原因是方法运行时出现验证错误 Seed 。 有关调试方法的提示,请参阅种子设定和调试实体框架 (EF) DBSeed

HTTP 500.19 错误

错误消息:

HTTP 错误 500.19 - 内部服务器错误
由于页面的相关配置数据无效,因此无法访问请求的页面。

解决方案:

获取此错误的一种方法是,具有解决方案的多个副本,每个副本都使用相同的端口号。 通常可以通过退出 Visual Studio 的所有实例来解决此问题,然后重启正在处理的项目。 如果不起作用,请尝试更改端口号。 右键单击项目文件,然后单击属性。 选择 “Web ”选项卡,然后在 “项目 URL ”文本框中更改端口号。

定位 SQL Server 实例出错

错误消息:

建立到 SQL Server 的连接时出现与网络相关或特定于实例的错误。 找不到或无法访问服务器。 请验证实例名称是否正确,SQL Server 是否已配置为允许远程连接。 (提供程序:SQL 网络接口,错误:26 - 定位指定服务器/实例出错)

解决方案:

检查συμβολοσειρά σύνδεσης。 如果已手动删除数据库,请在构造字符串中更改数据库的名称。