教程:了解 MVC 5 Web 应用的高级 EF 方案
在上一教程中,你实现了每个层次结构的表继承。 本教程介绍几个主题,在学习使用 Entity Framework Code First 的 ASP.NET Web 应用程序开发基础知识后,需要注意这些主题。 前几个部分提供分步说明,指导你完成代码和使用 Visual Studio 完成任务。
对于其中大多数主题,你将使用已创建的页面。 若要使用原始 SQL 进行批量更新,需要创建一个新页面,用于更新数据库中所有课程的学分数:
在本教程中,你将了解:
- 执行原始 SQL 查询
- 执行无跟踪查询
- 检查发送到数据库的 SQL 查询
你还将了解:
- 创建抽象层
- 代理类
- 自动脏值检测
- 自动验证
- 实体框架 Power Tools
- 实体框架源代码
先决条件
执行原始 SQL 查询
Entity Framework Code First API 包含可用于将 SQL 命令直接传递到数据库的方法。 你有以下选择:
- 对返回实体类型的查询使用 DbSet.SqlQuery 方法。 返回的对象必须是对象预期的
DbSet
类型,并且除非关闭跟踪,否则数据库上下文会自动跟踪它们。 (请参阅以下有关 AsNoTracking 方法的部分。) - 对于返回非实体类型的查询,请使用 Database.SqlQuery 方法。 数据库上下文不会跟踪返回的数据,即使你使用该方法来检索实体类型也是如此。
- 对非查询命令使用 Database.ExecuteSqlCommand 。
使用 Entity Framework 的优点之一是它可避免你编写跟数据库过于耦合的代码 它会自动生成 SQL 查询和命令,使得你无需自行编写。 但是,在需要运行手动创建的特定 SQL 查询时,存在一些异常情况,这些方法使你能够处理此类异常。
在 Web 应用程序中执行 SQL 命令时,请务必采取预防措施来保护站点免受 SQL 注入攻击。 一种方法是使用参数化查询,确保不会将网页提交的字符串视为 SQL 命令。 在本教程中,将用户输入集成到查询中时会使用参数化查询。
调用返回实体的查询
DbSet<TEntity> 类提供了一个方法,可用于执行返回 类型TEntity
实体的查询。 若要查看其工作原理,请更改控制器的 Department
方法中的Details
代码。
在 DepartmentController.cs 的 方法中Details
,将db.Departments.FindAsync
db.Departments.SqlQuery
方法调用替换为方法调用,如以下突出显示的代码所示:
public async Task<ActionResult> Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
// Commenting out original code to show how to use a raw SQL query.
//Department department = await db.Departments.FindAsync(id);
// Create and execute raw SQL query.
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);
}
为了验证新代码是否正常工作,请选择“院系”选项卡,然后选择其中某一院系的“详细信息” 。 确保所有数据都按预期显示。
调用返回其他类型的对象的查询
之前你在“关于”页面创建了一个学生统计信息网格,显示每个注册日期的学生数量。 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.cs 中,将 方法中的 About
LINQ 语句替换为 SQL 语句,如以下突出显示的代码所示:
public ActionResult About()
{
// Commenting out LINQ to show how to do the same thing in SQL.
//IQueryable<EnrollmentDateGroup> = from student in db.Students
// group student by student.EnrollmentDate into dateGroup
// select new EnrollmentDateGroup()
// {
// EnrollmentDate = dateGroup.Key,
// StudentCount = dateGroup.Count()
// };
// SQL version of the above LINQ code.
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
IEnumerable<EnrollmentDateGroup> data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
return View(data.ToList());
}
运行“关于”页。 验证它是否显示与之前相同的数据。
调用更新查询
假设 Contoso University 管理员希望能够在数据库中执行批量更改,例如更改每个课程的学分数。 如果该大学提供了大量课程,那么将所有课程作为实体来检索并单独更改就非常低效。 在本部分中,你将实现一个网页,该网页使用户能够指定更改所有课程的学分数的因素,并且你将通过执行 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
将调用 方法,并在 multiplier
文本框中输入值。 然后,该代码执行更新课程的 SQL,并将受影响的行数返回到变量中的 ViewBag.RowsAffected
视图。 当视图获取该变量中的值时,它将显示更新的行数,而不是文本框和提交按钮。
在 CourseController.cs 中,右键单击其中 UpdateCourseCredits
一种方法,然后单击“ 添加视图”。 此时将显示 “添加视图 ”对话框。 保留默认值,然后选择“ 添加”。
在 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
)。 在文本框中输入数字:
单击“更新”。 可以看到受影响的行数。
单击“返回列表”可以查看课程列表,其中学分已替换为修改后的数字。
有关原始 SQL 查询的详细信息,请参阅 MSDN 上的 原始 SQL 查询 。
非跟踪查询
当数据库上下文检索表行并创建表示它们的实体对象时,默认情况下,它会跟踪内存中的实体是否与数据库中的内容同步。 更新实体时,内存中的数据充当缓存并使用该数据。 在 Web 应用程序中,此缓存通常是不必要的,因为上下文实例通常生存期较短(创建新的实例并用于处理每个请求),并且通常在再次使用该实体之前处理读取实体的上下文。
可以使用 AsNoTracking 方法禁用对内存中实体对象的跟踪。 可能想要执行的典型方案包括以下操作:
- 查询检索的数据量如此之大,关闭跟踪可能会明显提高性能。
- 你想要附加一个实体以更新它,但你之前出于不同的目的检索了同一个实体。 由于数据库上下文已跟踪了该实体,因此无法附加要更改的实体。 处理这种情况的一种方法是将
AsNoTracking
选项与前面的查询一起使用。
有关演示如何使用 AsNoTracking 方法的示例,请参阅 本教程的早期版本。 本教程的此版本不会在 Edit 方法中对模型绑定程序创建的实体设置 Modified 标志,因此不需要 AsNoTracking
。
检查发送到数据库的 SQL
有时能够以查看发送到数据库的实际 SQL 查询对于开发者来说是很有用的。 在前面的教程中,你了解了如何在侦听器代码中执行此操作;现在,你将看到一些无需编写侦听器代码即可完成此操作的方法。 若要尝试此操作,你将查看一个简单的查询,然后查看在添加预先加载、筛选和排序等选项时会发生什么情况。
在 Controllers/CourseController 中,将 方法替换为 Index
以下代码,以便暂时停止预先加载:
public ActionResult Index()
{
var courses = db.Courses;
var sql = courses.ToString();
return View(courses.ToList());
}
现在, return
在语句上设置一个断点 (F9,光标位于该行) 。 按 F5 在调试模式下运行项目,然后选择“课程索引”页。 当代码到达断点时,检查 sql
变量。 你将看到发送到 SQL Server 的查询。 这是一个简单的 Select
语句。
{SELECT
[Extent1].[CourseID] AS [CourseID],
[Extent1].[Title] AS [Title],
[Extent1].[Credits] AS [Credits],
[Extent1].[DepartmentID] AS [DepartmentID]
FROM [Course] AS [Extent1]}
单击放大镜以查看 文本可视化工具中的查询。
现在,你将向“课程索引”页添加一个下拉列表,以便用户可以筛选特定部门。 你将按标题对课程进行排序,并为导航属性指定预先加载 Department
。
在 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
断点。
方法接收 参数中下拉列表的 SelectedDepartment
选定值。 如果未选择任何内容,则此参数将为 null。
包含 SelectList
所有部门的集合将传递到下拉列表的视图。 传递给 SelectList
构造函数的参数指定值字段名称、文本字段名称和所选项。
Get
对于存储库的 Course
方法,代码指定筛选表达式、排序顺序以及导航属性的Department
预先加载。 如果在下拉列表中未选择任何内容,则筛选器表达式始终返回 true
, (SelectedDepartment
为 null) 。
在 Views\Course\Index.cshtml 中,紧邻开始 table
标记之前,添加以下代码以创建下拉列表和提交按钮:
@using (Html.BeginForm())
{
<p>Select Department: @Html.DropDownList("SelectedDepartment","All")
<input type="submit" value="Filter" /></p>
}
在仍设置断点的情况下,运行“课程索引”页。 继续执行代码第一次命中断点的时间,以便页面显示在浏览器中。 从下拉列表中选择一个部门,然后单击“ 筛选”。
这一次,第一个断点将是针对下拉列表的部门查询。 跳过该代码, query
并在代码下次到达断点时查看变量,以查看 Course
查询现在的外观。 你将看到如下所示的内容:
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
可以看到,查询现在是一个与数据一JOIN
起Course
加载Department
数据的查询,并且它包含 子WHERE
句。
删除行 var sql = courses.ToString()
。
创建抽象层
许多开发人员编写代码实现存储库和工作模式单元以作为使用 Entity Framework 代码的包装器。 这些模式用于在应用程序的数据访问层和业务逻辑层之间创建抽象层。 实现这些模式可让你的应用程序对数据存储介质的更改不敏感,而且很容易进行自动化单元测试和进行测试驱动开发 (TDD)。 但是,出于以下几个原因,编写其他代码来实现这些模式并不总是最佳选择:
- EF 上下文类可以为使用 EF 的数据库更新充当工作单位类。
- 对于使用 EF 进行的数据库更新,EF 上下文类可充当工作单元类。
- Entity Framework 6 中引入的功能使无需编写存储库代码即可更轻松地实现 TDD。
有关如何实现存储库和工作单元模式的详细信息,请参阅 本系列教程的 Entity Framework 5 版本。 有关在 Entity Framework 6 中实现 TDD 的方法的信息,请参阅以下资源:
代理类
例如,当实体框架 (创建实体实例时,当你执行查询) 时,它通常会将它们创建为动态生成的派生类型的实例,该派生类型充当实体的代理。 例如,请参阅以下两个调试器映像。 在第一张图像中,可以看到变量 student
在实例化实体后立即为预期 Student
类型。 第二张图像使用 EF 从数据库读取学生实体后,会看到代理类。
此代理类替代实体的某些虚拟属性,以插入挂钩,以便在访问属性时自动执行操作。 此机制用于的一个函数是延迟加载。
大多数情况下,你不需要知道代理的这种使用,但有一些例外情况:
- 在某些情况下,你可能希望阻止实体框架创建代理实例。 例如,在序列化实体时,通常需要 POCO 类,而不是代理类。 避免序列化问题的一种方法是将数据传输对象序列化 (DTO) 而不是实体对象,如 将 Web API 与实体框架配合使用 教程所示。 另一种方法是 禁用代理创建。
- 使用
new
运算符实例化实体类时,不会获得代理实例。 这意味着你不会获得延迟加载和自动更改跟踪等功能。 这通常可以:通常不需要延迟加载,因为要创建不在数据库中的新实体,并且如果显式将实体标记为Added
,则通常不需要更改跟踪。 但是,如果确实需要延迟加载并且需要更改跟踪,则可以使用 类的DbSet
Create 方法使用代理创建新的实体实例。 - 你可能想要从代理类型获取实际的实体类型。 可以使用 类的
ObjectContext
GetObjectType 方法获取代理类型实例的实际实体类型。
有关详细信息,请参阅在 MSDN 上使用 代理 。
自动脏值检测
Entity Framework 通过比较的实体的当前值与原始值来判断更改实体的方式 (因此需要发送更新到数据库)。 查询或附加该实体时会存储的原始值。 如下方法会导致自动脏值:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
如果要跟踪大量实体,并在循环中多次调用其中一种方法,则通过使用 AutoDetectChangesEnabled 属性暂时关闭自动更改检测,可能会显著提高性能。 有关详细信息,请参阅 自动检测 MSDN 上的更改。
自动验证
调用 SaveChanges
方法时,默认情况下,实体框架将在更新数据库之前验证所有已更改实体的所有属性中的数据。 如果已更新大量实体,并且已验证数据,则不需要执行此工作,并且可以通过暂时关闭验证来减少保存更改的过程所花费的时间。 可以使用 ValidateOnSaveEnabled 属性执行此操作。 有关详细信息,请参阅 MSDN 上的 验证 。
Entity Framework Power Tools
Entity Framework Power Tools 是一个 Visual Studio 加载项,用于创建这些教程中显示的数据模型关系图。 这些工具还可以执行其他功能,例如基于现有数据库中的表生成实体类,以便可以将数据库与 Code First 配合使用。 安装工具后,上下文菜单中会显示一些其他选项。 例如,在 解决方案资源管理器 中右键单击上下文类时,会看到 和 Entity Framework 选项。 这使你能够生成关系图。 使用 Code First 时,无法更改关系图中的数据模型,但可以移动内容以使其更易于理解。
Entity Framework 源代码
GitHub 上提供了 Entity Framework 6 的源代码。 可以提交 bug,并且可以为 EF 源代码提供自己的增强功能。
尽管源代码是开放的,但实体框架作为 Microsoft 产品完全受支持。 Microsoft Entity Framework 团队将控制接受哪些贡献和测试所有的代码更改,以确保每个版本的质量。
致谢
- Tom Dykstra 编写了本教程的原始版本,共同创作了 EF 5 更新,并编写了 EF 6 更新。 Tom 是 Microsoft Web 平台和工具内容团队的高级编程作家。
- Rick Anderson (twitter @RickAndMSFT) 做了大部分工作来更新 EF 5 和 MVC 4 教程,并共同创作 EF 6 更新。 Rick 是 Microsoft 的高级编程作家,专注于 Azure 和 MVC。
- Rowan Miller 和实体框架团队的其他成员协助进行代码评审,并帮助调试在更新 EF 5 和 EF 6 教程时出现的许多迁移问题。
解决常见问题
无法创建/卷影副本
错误消息:
当该文件已存在时,无法创建/隐藏“filename>”副本<。
解决方案
等待几秒钟,然后刷新页面。
无法识别Update-Database
从 PMC) Update-Database
中的 命令 (错误消息:
术语“Update-Database”不能识别为 cmdlet、函数、脚本文件或可操作程序的名称。 请检查名称的拼写,如果包含路径,请验证该路径是否正确,并重试。
解决方案
退出 Visual Studio。 重新打开项目,然后重试。
验证失败
从 PMC) Update-Database
中的 命令 (错误消息:
一个或多个实体的验证失败。 有关更多详细信息,请参阅“EntityValidationErrors”属性。
解决方案
此问题的一个原因是方法运行时出现 Seed
验证错误。 有关调试方法的提示,请参阅 将实体框架 (EF) 数据库进行种子设定和 调试 Seed
。
HTTP 500.19 错误
错误消息:
HTTP 错误 500.19 - 内部服务器错误:无法访问请求的页面,因为页面的相关配置数据无效。
解决方案
出现此错误的一种方法是具有解决方案的多个副本,其中每个副本使用相同的端口号。 通常可以通过退出 Visual Studio 的所有实例,然后重启正在处理的项目来解决此问题。 如果这不起作用,请尝试更改端口号。 右键单击项目文件,然后单击“属性”。 选择“ Web ”选项卡,然后在“ 项目 URL” 文本框中更改端口号。
定位 SQL Server 实例出错
错误消息:
建立到 SQL Server 的连接时出现与网络相关或特定于实例的错误。 找不到或无法访问服务器。 请验证实例名称是否正确,SQL Server 是否已配置为允许远程连接。 (提供程序:SQL 网络接口,错误:26 - 查找指定的服务器/实例时出错)
解决方案
请检查连接字符串。 如果已手动删除数据库,请在构造字符串中更改数据库的名称。
获取代码
其他资源
有关如何使用实体框架处理数据的详细信息,请参阅 MSDN 上的 EF 文档页 和 ASP.NET 数据访问 - 推荐资源。
有关如何在生成 Web 应用程序后部署应用程序的详细信息,请参阅 MSDN 库中 ASP.NET Web 部署 - 建议的资源 。
有关与 MVC 相关的其他主题(例如身份验证和授权)的信息,请参阅 ASP.NET MVC - 建议的资源。
后续步骤
在本教程中,你将了解:
- 已执行原始 SQL 查询
- 执行了无跟踪查询
- 已检查发送到数据库的 SQL 查询
你还了解了:
- 创建抽象层
- 代理类
- 自动脏值检测
- 自动验证
- Entity Framework Power Tools
- Entity Framework 源代码
完成本系列教程,介绍如何在 ASP.NET MVC 应用程序中使用实体框架。 若要了解 EF Database First,请参阅 DB First 教程系列。