高级主题 - EF Core 与 ASP.NET Core MVC 教程 (10 of 10)
作者 Tom Dykstra 和 Rick Anderson
Contoso 大学 Web应用程序演示了如何使用 Entity Framework Core 1.1 以及 Visual Studio 2017 来创建 ASP.NET Core 1.1 MVC Web 应用程序。更多信息请参考 第一节教程.
在前一篇教程中我们实现了基于 TPH 的继承。而这篇教程则将介绍几种实用主题,它们将在你掌握利用 Entity Framework Core 开发 ASP.NET Web 应用程序的基础后非常管用。
原生 SQL 查询
使用 Entity Framework 的一大好处在于能避免代码与存储数据的特定方法之间的耦合。通过生成 SQL 查询和命令,我们也可以避免直接编写这些 SQL 语句。但是也有例外场景,当你需要运行特定 SQL 查询的时候,你就必须手工创建 SQL 查询了。在这些场景下,Entity Framework Code First API 包含直接传递 SQL 命令的方法。在 EF Core 1.0 中你有这些选择:
- 为返回实体类型的查询使用
DbSet.FromSql
方法。所返回的对象必须是DbSet
对象期望获得的类型,并且它们将自动被数据库上下文跟踪,出给你 关闭跟踪。
- 为非查询命令使用
Database.ExecuteSqlCommand
。
如果你需要运行能返回非实体类型结果的查询,你可以从 EF 中获得数据库连接,而后通过 ADO.NET 来获取。返回的数据将不会被数据库上下文跟踪,即使你检索的是实体类型。
当你在 Web 应用程序中执行 SQL 命令时,你必须确保采取预防措施保护网站被执行 SQL 注入攻击。其中一种策略是使用参数化查询以确保通过网页提交的字符串不能被解释为 SQL 命令。在本教程中你将使用参数化查询来之兴用于输入的请求。
调用返回实体的请求
DbSet<TEntity>
c类提供了执行查询并返回 TEntity
实体结果的方法。你可以通过修改 Department 控制器中的 Details
方法来查看它的工作原理。
在 DepartmentController.cs 文件的 Details
方法中,将代码替换为通过调用 FromSql
方法检索系,如下例所示(注意代码高亮处):
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}
为了验证新代码能正常工作,请选择 Departments 标签,然后随意一个系的 Details。
Call a query that returns other types
调用返回其它类型的请求
之前你已经在 About 页中创建了用于显示每个学生的入学时间的统计表。你从 Students 实体集(_context.Students
)中获取数据并使用 LINQ 将结果转换为 EnrollmentDateGroup
视图模型对象列表。假设你想让它自己编写 SQL 语句而不是使用 LINQ,那么你需要执行一个 SQL 查询并返回一个非实体对象。在 EF Core 1.0 中,有一个办法做到这一点,就是从 EF 中取出数据库连接,然后编写 ADO.NET 代码。
在 HomeController.cs 文件中,用 ADO.NET 代码替换 About
方法中的 LINQ 语句,并如下高亮处代码所示:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}
添加一个 using 语句:
using System.Data.Common;
运行 About 页面。它所显示的页面与更改前的别无二致。
调用 Update 查询
假设 Contoso University 的管理员需要在数据库内做全局变更,比如修改每门课程的学分。如果大学里有数量庞大的课程,那么将这些课程以实体的方式逐个检索出并挨个更新它们将是何等低效。在本节中你将实现一个页面,它能让用户为所有课程的学分变更指定一个因数,然后通过执行 SQL UPDATE 语句更新数据。这个网页看上去如下图所示:
在 CoursesContoller.cs 中添加 HttpGet 和 HttpPost 的 UpdateCourseCredits 方法:
public IActionResult UpdateCourseCredits()
{
return View();
}
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}
当控制器处理 HttpGet 请求时, ViewData["RowsAffected"]
不返回任何东西,视图会显示一个空文本框和一个提交按钮,如前图所示。
点击 Update 按钮,调用 HttpPost 方法,然后就会乘上文本框中输入的值。代码将执行 SQL,在视图的 ViewData
中更新课程数据,返回受影响行数并通过 RowsAffected
传递给视图上。视图从该变量获得值后将其显示于页面上。
在 Solution Explorer 中右键点击 Views/Courses 文件夹,然后点击 Add > New Item。
在 Add New Item 对话框中点击 ASP.NET(在左侧栏的 Installed 下),点击 MVC View Page,将新视图命名为 UpdateCourseCredits.cshtml。
用如下代码替换 Views/Courses/UpdateCourseCredits.cshtml 中的模板代码:
@{
ViewBag.Title = "UpdateCourseCredits";
}
<h2>Update Course Credits</h2>
@if (ViewData["RowsAffected"] == null)
{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default" />
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
通过选择 Courses 标签页,然后在地址栏的 URL 末尾添加 "/UpdateCourseCredits" (如 http://localhost:5813/Course/UpdateCourseCredits)
),而后应用程序将会运行 UpdateCourseCredits
方法。在文本框中输入一个数字:
点击 Update。你可以看到受影响的行数:
点击 Back to List,查看学分修订后的课程列表。
请注意,生产环境代码将确保更新总是有效的结果数据。 这里显示的简化版的代码可以将信用数量乘以大于5的数字( Credits
属性具有 [Range(0, 5)]
]特性。)更新查询将工作,但无效数据可能在系统的其他部分导致意外的结果,假设学分数为5或更少。
更多有关原生 SQL 查询的资料参见 原生 SQL 查询。
检查发送到数据库的 SQL 语句
有时,可以看到发送到数据库的实际执行的 SQL 查询是非常有用的。ASP.NET Core 内建日志功能可以由 EF Core 自动使用,用于写入是包括 SQL 查询与更新的日志。本节将提供几个 SQL 日志记录的例子。
打开 StudentController.cs,在 Details
方法的 if (student == null)
语句上设置一个断点。
以调试模式运行应用程序,转到某个学生的 Details 页面。
转到显示调试输出的 Output 窗口,你会看到查询:
Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory:Information: Executed DbCommand (225ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [e].[EnrollmentID], [e].[CourseID], [e].[Grade], [e].[StudentID], [c].[CourseID], [c].[Credits], [c].[DepartmentID], [c].[Title]
FROM [Enrollment] AS [e]
INNER JOIN (
SELECT DISTINCT TOP(2) [s].[ID]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
) AS [s0] ON [e].[StudentID] = [s0].[ID]
INNER JOIN [Course] AS [c] ON [e].[CourseID] = [c].[CourseID]
ORDER BY [s0].[ID]
你在这里可能会吃惊:SQL 最多选选择 2 行记录(TOP(2)
)。 SingleOrDefaultAsync
方法不能解析为服务器上的一行。如果 WHERE 子句命中多行记录,该方法必然返回 null,因此 EF 只能选择最多两行——这是因为不管命中两行还是更多行, SingleOrDefaultAsync
方法返回的结果是一样的。
这里需要注意,你不需要使用调式模式,便能在断点处停下并将日志输出到 Output 窗口中。这是一种便捷的在记录日志的地方停下以便查看输出的方法。如果不这么做,日志会继续记录下去,而你必须向后滚动才能找到你所感兴趣的那部分日志。
存储库(Repository)与工作单元(UoW)模式
许多开发者编写代码来实现存储库(repository)和工作单元(unit of work)模式作为 Entity Framework 的封装。这类模式一般会在应用程序的数据访问层(data access layer)和业务逻辑层(business logic layer)之间创建一层抽象层(abstraction layer)。实现这类模式固然可以是应用程序与数据库存储解耦,并可促进自动化单元测试(automated unit testing)或测试驱动开发(test-driven development,TDD),但通过编写额外的代码来实现这类模式并不是 EF 金科玉律般的最佳时间,因为:
- EF 上下文类本身就将你所写的代码与数据库存储特定代码(data-store-specific code)相隔离。
- EF 上下文可以充当你利用 EF 更新数据库时的工作单元类。
- EF 包含不编写存储库代码时实现 TDD 的功能。
有关如何实现存储库与工作单元模式的资料,请参见 Entity Framework 5 系列教程。
Entity Framework Core 实现了一种可用于测试的内存数据库提供程序。有关这方面的资料请参见 在内存中测试。
自动检测变更
Entity Framework 确定一个实体是否发生变更(并因此需要将更新发送到数据库)的方法,是通过比较实体当前值与原始值。当实体被查询或附加时,原始植会被保存下来。引发自动变更检测的方法有以下几种:
DbContext.SaveChanges
DbContext.Entry
ChangeTracker.Entries
如果你正跟踪大量实体,并且在循环体中多次调用上述列举的方法,那么你可以通过 ChangeTracker.AutoDetectChangesEnabled
属性暂时关闭自动变更检测以获得性能上的提升。例如:
_context.ChangeTracker.AutoDetectChangesEnabled = false;
Entity Framework Core 源码以及开发计划
Entty Framework Core 的源代码在 https://github.com/aspnet/EntityFramework。除了源代码,你还可以得到每日构建、问题跟踪、细节说明、设计会议笔记, 未来开发发展路线图等。你可以提交 Bug,并且可以贡献自己的增强 EF 源代码。
虽然很多源码是开放的,Entity Framework Core 是一个完全由微软支持的产品。微软的 Entity Framework 团队保持对所有代码变更贡献的接受和测试的控制权以确保每个释出版本的质量。
从现有数据库逆向工程
对包含实体类型的数据模型进行反向工程,请使用 scaffold-dbcontext command 命令。具体请阅读 开始学习。
本系列的第三个教程 显示了如何通过在 至此,有关在 ASP.NET MVC 应用程序中使用 Entity Framework Core 的系列教程到此结束。 更多有关 EF Core 的资料请参见 Entity Framework Core 文档。还有这本书 Entity Framework Core in Action。 有关部署 Web 应用程序的信息,请参见 发布与部署. 有关 ASP.NET Core MVC 的其他话题,比如身份认证与授权等,请参见 ASP.NET Core 文档。 本教程由 Tom Dykstra 和 Rick Anderson (twitter @RickAndMSFT) 编写,由 dotNET Core Studying Group 翻译为中文。
Entity Framework 团队的 Rowan Miller、Diego Vega 以及其他诸位同僚帮助我们进行代码审查,并帮我们就本系列教程进行调试。 错误信息: Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process. 解决方案: 在 IIS Express 中停止站点:转到 Windows System Tray(Windows 系统托盘区),找到 IIS Express 并右键单击图标,选择 Contoso University 站点,然后点击 Stop Site。 导致原因: EF CLI 命令不会自动关闭并保存代码文件。如果在你运行 解决方案: 运行 在一个存在数据的数据库中修改架构可能会得到其他错误。如果你得到一个无法处理的迁移错误,你可以:1)修改连接字符串上的数据库名称,2)删除数据库。这样当新的数据库创建时,就不会有数据需要迁移,update-database 命令也就更易无错完成了。 最简单的办法是在 appsettings.json 修改数据库的名称。下一次你运行 在 SSOX 中删除数据库的话,右键点击数据库,然后选择 Delete,接着在 Delete Database 对话框中选择 Close existing connections 并点击 OK。 通过运行 `database drop 这条 CLI 命令删除数据库: 错误信息: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified) 解决方案: 检查连接字符串。如果你手动删除了数据库文件,那么需要修改连接字符串中数据库的名字以便能重新开建一个新的数据库。使用动态 LINQ 简化排序选择代码
switch
语句中硬编码列名来编写 LINQ 代码。 有两列可供选择,这可以正常工作,但如果您有很多列,代码可能会更复杂。 要解决这个问题,可以使用 EF.Property
方法来指定属性的名称作为一个字符串。 要尝试这种方法,请使用以下代码替换 StudentsController
中的 Index
方法public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? page)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}
if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
page ?? 1, pageSize));
}
下一步
致谢
常见错误
ContosoUniversity.dll 被另一个进程使用
Migration scaffolded with no code in Up and Down methods
migrations add
命令时没有保存修改,EF 是不会发现你做的修改的。migrations remove
命令,然后保存你的代码,再运行 migrations remove
命令。执行数据库更新时发生错误
database update
的时候新数据库会被创建。dotnet ef database drop
错误定位 SQL Server 实例