本篇原文链接:Advanced Entity Framework Scenarios
本篇主要讲一些使用Code First建立ASP.NET WEB应用的时候除了基础的方式以外的一些扩展方式方法:
1、Performing Raw SQL Queries (执行真正的SQL语句)
2、Performing no-tracking queries (执行无跟踪的SQL语句)
3、Examining SQL sent to the database (检查发到数据库的SQL语句)
Performing Raw SQL Queries
EF Code First 有API 可以让用户直接把SQL语句发给数据库去执行,有以下几种选择:
1、DbSet.SqlQuery 执行查询语句后返回实体类型(即DbSet对象所预期的);同时被数据库上下文自动跟踪(除非手动关闭,可参看AsNoTracking 方法)
2、Database.SqlQuery 可以用来执行查询语句后返回不是实体类型的,同时也不会被数据库上下文跟踪,即便是返回的是实体类型;
3、Database.ExecuteSqlCommand 执行非查询的SQL语句
用EF的一个好处就是一些重复的语句可以不用自己再写,它可以为你生成一些SQL语句,可以解放你不用自己再写;
但是也有一些场景,需要你自己运行自己手动创建的SQL或者方法来处理一些特殊异常情况。
在WEB网页应用的时候,必须预防SQL注入攻击。最好的办法就是用参数化查询语句,而不是拼接字符串。
Calling a Query that Returns Entities
DbSet<TEntity>类提供了一个方法可以用来执行一个SQL语句,并返回一个实体类型。
简单的例子:
public async Task<ActionResult> Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
} //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);
}
Calling a Query that Returns Other Types of Objects
在前面一个章节设计了Home/About 里面的代码:
public ActionResult About()
{
IQueryable<EnrollmentDateGroup> data = from student in db.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(data.ToList());
}
可以替换为:
public ActionResult About()
{
//IQueryable<EnrollmentDateGroup> 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";
IEnumerable<EnrollmentDateGroup> data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
return View(data.ToList());
}
运行起来,效果是一样的。
Calling an Update Query
假设需要一个页面用来批量调整 Course的 Credits;
在Course控制器增加两个Action:
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();
}
当Get UpdateCourseCredits请求的时候,就给用户一个编辑框和一个提交按钮;
当用户输入后,点击提交后,返回带有更新了多少条记录的显示信息的页面;
为UpdateCourseCredits建一个空的视图,然后用下面代码代替:
@model EFTest.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>
把应用执行起来后,进入Course页面,然后在后面加UpdateCourseCredits
访问如 http://XXXXXXX/Course/UpdateCourseCredits 这个URL:
然后输入2后,点Update:
会显示一共多少记录被更新;然后返回Course主页面看到所有的都乘以2了:
更多关于执行原生SQL语句的内容见: Raw SQL Queries
No-Tracking Queries
当一个数据库上下文检索数据表行并建立一个实体对象用来表示它的时候,默认是跟踪在内存里的实体和在数据库里的保持同步。
在内存里的数据就像缓存用来更新实体数据。而缓存在WEB应用中不是必须的,因为上下文实例是典型的短生命周期的,每次请求会新建一个,用完就disposed,在实体数据被再使用的时候,上一个上下文实例已经被disposed了。
所以可以用AsNoTracking方法来不跟踪内存里的实体对象。
典型的场景包括:
1、一次查询很大量的数据,如果要跟踪,就是极大的降低性能;
2、当准备附加一个实体计划更新时,如果因为其他原因,已经检索同一个实体,而因为该实体被数据库上下文所跟踪,所以你就不可以附加这个实体。唯一的办法就是在之前的检索中采用AsNoTracking方式。
这里原文说到以前的教程中做了这个测试,并提供了链接;
我在这里按照以前的教程做这个测试:
先为Department 控制器加一个私有方法来检查数据库,是不是这个Administrator已经对应了一个Department,如果已经对应,则直接提示错误;
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);
}
}
}
把Edit HttpPost Action 改成以下简单的方式,原先检查并发的先注释掉:
[HttpPost]
[ValidateAntiForgeryToken]
public 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;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
}
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", department.InstructorID);
return View(department);
}
下面进行测试:先编辑一个Department,然后把Administrator选为和另一个Department一样,这个时候就不会继续执行SaveChange的操作了,而是直接报错:
如果返回到部门主页,再点击编辑,这次随便编辑其他值,比如Budget :
就会出现错误:
就是因为附件这个实体的时候,前面检查Administrator的时候已经去数据库查询所有Administrator 为 Kapoor, Candace 的Dempartment,导致Economics部门已经被读取,并已经跟踪;
当Edit Action尝试为 MVC自动绑定的Department 实体 更改标识的时候,会导致这个错误,因为这个Department 已经被读取,并被跟踪。
要解决这个问题,也很简单,在ValidateOneAdministratorAssignmentPerInstructor
方法中,加入AsNoTracking(),如下:
var duplicateDepartment = db.Departments
.Include("Administrator")
.Where(d => d.PersonID == department.PersonID)
.AsNoTracking()
.FirstOrDefault();
这样改好就没有问题了。
Examining SQL sent to the database
有时候需要能够看到实际发到数据库的SQL语句,对于调试程序有很大帮助,前面的教程讲了采用拦截的方式,把一些信息拦截到Output去查看;
下面准备说一个简单的方案来查看实际发到数据库的SQL语句:
把Course的Index Action变为以下简单的方式:
public ActionResult Index()
{ var courses = db.Courses;
var sql = courses.ToString();
return View(courses.ToList()); //var courses = db.Courses.Include(c => c.Department);
//return View(courses.ToList());
}
然后在 return语句加上断点: 选中行点F9即可:
然后按F5执行应用,点击进入Course\Index页面,会停留在这一行:
然后光标停留在上一行sql上面,就可以看到sql 的值:
点那个放大镜图标,可以看的详细点:
下面为Course Index页面加一个Department 下拉式的过滤;
修改Course Index Action 为:
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());
}
再在Index视图 Table元素前面加上:
@using (Html.BeginForm())
{
<p>Select Department: @Html.DropDownList("SelectedDepartment","All")
<input type="submit" value="Filter" /></p>
}
效果如图:
可以在return语句加上断点:
这个时候运行到这里的时候,就可以看sql的值:(加入了Department的查询语句)
Repository and unit of work patterns
仓库和单元工作模式是很多开发者会去写代码实现的,在数据访问层和业务逻辑层之间加入一个虚拟层;
这种模式可以帮助应用与数据存储变化隔离开,可促进实现单元测试或者TDD test-driven development;
然而,在使用EF的情况下,再额外写代码实现这个模式已经不是最好的方式了,主要是以下原因:
1、EF 的上下文类已经实现应用代码和数据交互部分的隔离;
2、EF的上下文类可以作为单元工作类来进行数据库更新;
3、EF6 可以让这种模式实现起来更简单,而不用再自己写仓库代码;
更多学习仓库和单元工作模式,可以参考: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
Proxy classes 代理类
当EF创建一个实体实例的时候,它一般会为这个实体动态生成一个衍生类型作为代理,然后再创建这个代理的实例;
可以看原文中的两个图,第1个图,可以看到Student申明的时候是Student 类型,但到第2步,从数据库读取数据后,就可以看到代理类:
代理类重载了一些虚拟导航属性,用一些执行动作的钩子来填充,当这些虚拟属性被访问的时候,可以自动执行;这种机制主要是为了 延时加载;
大部分情况下,不需要关注代理类的工作,但是以下情况是特例:
1、在一些场景下,需要阻止EF产生代理类,比如你准备序列化实体的时候,肯定是希望是POCO类,而不是代理类;有一种办法来解决这个序列化问题,就是用序列化DTO而不是序列化实体对象;如Using Web API with Entity Framework;(一般在WEB API里序列化实体要求比较多); 另外也可以直接关闭代理类:disable proxy creation.
2、当直接用new来实例化一个实体的时候,你得到的不是代理类,这以为着你得不到延时加载、数据变化跟踪这些功能;这个应该也不是大问题,毕竟这个不是从数据库里取的数据,通常不需要延时加载,如果你明确定义它为Added ,你也不需要数据跟踪。 然而,你如果需要延时加载、数据变化跟踪,可以用DbSet的Create方法来新建代理类;
3、如果你需要从一个代理类实例获取真正的实体类型,可以使用ObjectContext
的 GetObjectType方法从一个代理类实例获取实体类型;
更多代理类信息参考:Working with Proxies
Automatic change detection 自动变化侦测
EF通过比较当前值和原始值来确定哪些实体变化了(因而要更新到数据库里),原始值是在被检索或者附加的时候存储,一些方法引发自动变化侦测:
- DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
如果在跟踪很多实体,并且在一个循环操作中,要Call上面这些方法很多次的话,那么临时性关闭数据变化侦测(通过使用AutoDetectChangesEnabled这个属性值)是可以带来很大的性能提升的;
更多信息见:Automatically Detecting Changes
Automatic validation 自动数据校验
EF默认会在SaveChange的时候校验数据,而如果是很大量的数据,而且已经被校验过了,那么可以通过临时性关闭自动校验(通过使用ValidateOnSaveEnabled这个属性值)来提高性能;
更多信息见: Validation