原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application
1.执行原生SQL查询:
EF Code First API包含直接传递SQL命令到数据库的方法:
- 使用DbSet.SqlQuery方法查询返回实体类型。返回的对象必须是DbSet对象所期望的类型,并且数据库上下文会自动追踪它们除非我们关闭了追踪。
- 使用Database.SqlQuery方法查询返回非实体类型。返回的数据不会被数据库上下文追踪,即使我们为该方法指定实体类型。
- 使用Database.ExecuteSqlCommand执行非查询操作。
使用EF的其中一个优点是避免了将我们的代码与特定的存储数据的方法联系的过于紧密。该优点通过为我们产生SQL查询和命令实现,这同时可以让我们不必自己编写这些语句。但在特殊情况下,我们需要执行自己编写的SQL语句,并且这些方法是我们能够处理这些异常。
长久以来当我们在web应用程序中执行SQL命令时,我们必须采取措施来防止SQL注入攻击 。一种方法是使用参数化的查询来取保web网页提交的字符串不会被解析为SQL命令。在本教程我们将整合用户输入为参数化的查询。
1.1.调用查询返回实体:
DbSet<TEntity>类提供了一个执行查询返回一个TEntity
实体类型的方法。
修改DepartmentController.cs的Details方法:
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);
}
1.2.调用查询返回其他类型的对象:
之前我们在About页面创建了一个student统计表格,显示每个enrollment date的student数量。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()
{
// 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());
}
1.3.调用Update查询:
在CourseContoller.cs添加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();
}
添加视图:
@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>
运行:
更多关于原生SQL查询信息请查看:Raw SQL Queries。
2.无追踪(No-Tracking)查询:
当一个数据库上下文搜索到表中的行数据会为他们创建实体对象,默认情况下它会一直追踪内存中的实体是否和数据库中的数据保持一致。内存中的数据作为缓存和更新实体时使用。这种缓存在web应用程序中通常是没有必要的,因为上下文实例通常是短暂的(每次请求都会新建和销毁一个上下文实例),并且通常上下文读取的实例在该实例再次被使用前就已经销毁 。
我们可以使用AsNoTracking方法禁用内存对象中实体对象的追踪。在下面几个典型的场景中我们可能想要这么做:
- 一个查询检索大量的数据,关闭追踪会显著的提高性能。
- 当我们为了更新而附加一个实体时,但是我们之前为了其他的目的已经获取了该实体。因为该实体已经能够被数据库上下文追踪,我们不能附加这个想要修改的实体。这种情况下的一种处理方式是对之前的查询使用
AsNoTracking
选项。
AsNoTracking
方法使用实例请参考:the earlier version of this tutorial。本教程在Edit方法的实体模型-绑定-创建时没有设置修改标志,所以没有必要使用AsNoTracking
。
3.检查发送到数据库的SQL:
修改Controllers/CourseController的Index方法,暂时禁用显式加载:
public ActionResult Index()
{
var courses = db.Courses;
var sql = courses.ToString();
return View(courses.ToList());
}
在return语句设置断点,运行程序,查看sql变量的值:
在Course的Index页面添加下拉列表,用于删选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语句设置断点。
修改Views\Course\Index.cshtml,在table标签之前:
@using (Html.BeginForm())
{
<p>Select Department: @Html.DropDownList("SelectedDepartment","All")
<input type="submit" value="Filter" /></p>
}
运行:
查看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
4.仓储和工作单元模式:
许多开发者编写代码实现仓储和工作单元模式作为包装器代码配合EF使用。这些模式的目的是在数据访问层和业务逻辑层之间创建一个抽象层。实现这种模式可以将程序与数据存储变化隔离,并且有利于单元测试和测试驱动开发(TDD)。但是编写额外的代码实现这些模式,对使用EF的程序并不总是最好的选择,原因如下:
- EF上下文类本身就实现了代码和数据存储的隔离。
- 当使用EF更新数据库时,EF上下文类可以当作工作单元类。
- EF6的特性使不编写仓储代码实现TDD变得更加容易。
更多关于实现仓储和工作单元模式信息请参考:the Entity Framework 5 version of this tutorial series。更多关于在EF6中实现TDD信息请参考:
- How EF6 Enables Mocking DbSets more easily
- Testing with a mocking framework
- Testing with your own test doubles
5.代理类:
当EF创建实体实例时(例如,执行查询时),通常把它们创建可以作为实体代理的动态的派生类型。例如,下面两张调试图片。从第一张可以看到student变量在实例化实体后直接是所期望的Student类型。在第二张图片中,当EF被用来从数据库读取一个student实体时使用的是代理类:
代理类覆盖了实体的一些virtual属性,用来插入钩子在访问属性时自动执行操作。这种机制的一个功能是用于延迟加载。
大部分情况下,我们不用关心这些代理的使用,除了一些几种情况:
- 在一些场景中,我们或许想要阻止EF创建代理实例。例如,当我们序列化产生的实体,需要POCO类时,不要代理类。一种解决序列化问题的做法是序列化数据传输对象(DTO)而不是实体对象,请参考:Using Web API with Entity Framework。另一种方法是:disable proxy creation。
- 当我们使用new实例化一个实体类时,不会产生代理实例。这意味着我们不使用 延迟加载和自动修改追踪功能。这通常是没有问题的,我们通常不需要延迟加载,因为我们不是在数据库中创建一个实体,并且我们通常也不需要修改追踪,如果我们特别标记实体为Added。然而,我们需要延迟加载和修改追踪,我们可以使用DbSet的Create方法创建使用代理的实体实例。
- 我们想要从代理类型获取实际的实体类型。我们可以使用
ObjectContext
类的GetObjectType方法从代理类型实例获取实际的实体类型。
更多信息请参考:Working with Proxies。
6.自动检测改变:
EF根据比较实体原始值和当前值实体来确定实体是否被改变(哪个update需要被发送的数据库)。原始值在实体被查询或附加时存储。下面的一些方法会自动检测改变:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
当我们追踪大量的实体,并且在循环中多次调用上面的方法时,使用AutoDetectChangesEnabled属性暂时关闭自动检测改变功能,来获取显著的性能提升。更多信息请查看:Automatically Detecting Changes。
7.EF Power Tools:
Entity Framework Power Tools是Visual Studio内置的用来创建本教程显示的数据模型图。该工具同样具有其他功能,例如根据已经存在的数据库的表产生实体类,这样就可以使用Code First。安装此工具后,上下文菜单会出现一些其他的选项。例如,当我们在Solution Explorer的上下文类中右键,会出现产生图表选项。当我们使用Code First时,我们不能改变图表中的数据模型,但是我们可以移动它们的位置,让它们变得容易理解。
8.EF源码:
EF6的源码在http://entityframework.codeplex.com/。除了源码,我们也可以获取nightly builds, issue tracking, feature specs, design meeting notes等。我们可以修复bug,并且贡献我们自己的EF6增强代码。
尽管EF是开源的,但是它是微软完全支持的产品。微软的EF团队控制接受哪些贡献的代码并对所有的更改进行测试以确保每个发行版本的质量。
9.总结:
更多关于如何使用EF处理数据的信息,请查看:EF documentation page on MSDN和ASP.NET Data Access - Recommended Resources。
更多关于如何部署编译后的web程序,请查看:ASP.NET Web Deployment - Recommended Resources。
更多关于MVC的其他话题,请查看:ASP.NET MVC - Recommended Resources。
10.常见错误以及它们的解决方案或者替代方案:
Cannot create/shadow copy
Error Message:
Cannot create/shadow copy '<filename>' when that file already exists.
Solution
等待几秒并刷新页面。
Update-Database not recognized
Error Message (from the Update-Database
command in the PMC):
The term 'Update-Database' is not recognized as the
name of a cmdlet, function, script file, or operable program. Check the spelling
of the name, or if a path was included, verify that the path is correct and try
again.
Solution
关闭Visual Studio,重启项目并重试。
Validation failed
Error Message (from the Update-Database
command in the PMC):
Validation failed for one or more entities. See
'EntityValidationErrors' property for more details.
Solution
这个问题的的其中一个原因是种子方法运行时验证错误. 查看Seeding and Debugging Entity Framework (EF) DBs关于调试Seed方法的建议。
HTTP 500.19 error
Error Message:
HTTP Error 500.19 - Internal Server Error
The requested page cannot be accessed because the related configuration data for
the page is invalid.
Solution
这个错误的一个原因是解决方案有多个副本,并且这些副本使用相同的端口号。通常我们可以通过关闭所有的Visual Studio,然后重启项目来解决此问题。如果上述方法不奏效,就改变端口号。右键项目,点击properties,选择Web标签在Project Url修改端口号。
Error locating SQL Server instance
Error Message:
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)
Solution
查看连接字符串。如果手动删除了数据库,则修改连接字符串的数据库名。