【EntityFramework系列教程十,翻译】ASP.NET MVC程序中的一些高级应用
前一章你已经完成了存储类以及单元工作类的设计,本章中将包含下列内容:
1)处理原生态的SQL语句请求。
2)处理“无跟踪”的请求。
3)测试发送到数据库的请求。
4)与代理类配合协同工作。
5)禁用自动变化检测机制。
6)禁用数据保存前验证机制。
以上大部分内容你将配合以创建的页面进行处理。为了使用原生态的SQL语句更新对应相关记录,您需要创建一个新页面用以批量更新数据库中全部课程学分所对应的那个编号。
为使用“无跟踪”请求,你需要额外对Department的Edit(编辑)页面追加一个新的验证逻辑:
【处理“原生态”SQL命令】
EntityFramework(代码优先)API方法允许你把SQL命令直接送入数据库中执行,你有以下一些选择:
1)使用DbSet.SqlQuery方法获取以实体集作为返回结果的方法:注意返回的对象必须是能够被DbSet所接受的类型,并且它们自动被数据库上下文所跟踪,除非你禁用该“跟踪”功能(参照以下关于“AsNoTracking
”方法)。
2)使用“DbDatabase.SqlQuery”方法以“非实体集”的形式返回查询结果。这些记录不被数据库上下文对象所跟踪——即便你设法使用了该方法返回了实体集的话。
3)使用DbDataBase.SqlCommand方法执行非“查询”的其它命令(增删改)。
使用EntityFramework的一个显著的好处就是在于你可以避免太过于靠近“处理存储数据”的某类方法,因为EntityFramework会为你自动生成对应SQL查询,使你从手动写这些代码中解脱出来。不过偶尔会有例外情况,可能你需要执行手动创建的SQL命令,这些方法或许对你而言可能用于处理特定的异常。
当你在一个网站应用程序中执行SQL命令时,你必须预防保护你的代码不受到SQL注入式攻击。方法之一就是采用参数化的查询语句以确保由网页所提交的字符串内容不能被解析为SQL命令。在本章中你将使用参数化查询方式把用户输入的内容整合在一起。
【调用一个返回实体集合的查询语句】
假设你想让你的“GenericRepository
”类在没有创建额外方法的情况下提供一些扩展性的排序或是过滤查询的功能的话,办法之一就是添加一个能够接受SQL原生态命令查询的方法。你可以在控制器中指定任意形式的过滤或是排序,像是依赖join或是子查询的Where条件等……在这部分你将看到如何实现这一类的方法:
在“GenericRepository.cs”中添加以下代码实现“GetWithRawSql”方法:
public virtual IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters) { return dbSet.SqlQuery(query, parameters).ToList(); }
在“CourseController.cs”中在Details方法中调用此新方法,如下所示:
public ActionResult Details(int id) { var query = "SELECT * FROM Course WHERE CourseID = @p0"; return View(unitOfWork.CourseRepository.GetWithRawSql(query, id).Single()); }
或许在此情况下你已经使用过GetById方法,但是你目前正在使用GetWithRawSql以确保该方法运行正确。
运行Details页,点击Course选项卡,然后点击具体某个课程的Details查看验证帅选方法是否正常工作:
【调用一个返回其它类型的查询语句】
早先你为About页创建学生统计信息是为了显示每个选课日期中包含多少名学生,此代码在“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方法:
var query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount " + "FROM Person " + "WHERE EnrollmentDate IS NOT NULL " + "GROUP BY EnrollmentDate"; var data = db.Database.SqlQuery<EnrollmentDateGroup>(query);
运行About页,如先前所示的一样:
【调用更新语句】
假设Contoso University想批量更新数据库中的一些数据(诸如为每个课程学分所对应的学号做更新)。如果该大学拥有大量的课程,那么把它们逐一从数据库中取出一个个修改显然效率非常低下。本节中你将实现一个页面,该页面将通过一个代理更新所有课程对应学分的那个编号,你将通过执行SQL更新命令实现这个任务。该页面看上去如下:
在先前的一章节中你使用存储类在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; } }
在“CourseContoller.cs”中,添加一个“UpdateCourseCredits”方法:
public ActionResult UpdateCourseCredits(int? multiplier) { if (multiplier != null) { ViewBag.RowsAffected = unitOfWork.CourseRepository.UpdateCourseCredits(multiplier.Value); } return View(); }
这个方法将同时作用于HttpGet与HttpPost方式。当HttpGet UpdateCourseCredits执行时,multiplier未赋值,自然页面上出现一个空的文本框和一个提交按钮;如先前所描述的一致。
当点击Update按钮之时,HttpPost方法得以执行。multiplier保存着曾经输入到文本框中的内容,随后代码调用存储方法“UpdateCourseCredits
”并返回受影响的行数,接着把这个结果存入ViewBag中;当页面接受到此ViewBag中的数据时,页面呈现该数字而不是文本框和提交按钮,如下所示:
在“Views\Course”文件夹中为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选项卡,手动在地址栏的Url后面追加“/UpdateCourseCredits”(比如:http://localhost:50205/Course/UpdateCourseCredits),在文本框中输入内容:
点击Update按钮,可以看到受影响(更新)的行数:
点击“Back To List(返回列表)”可以看到经过修正的数字信息:
有关更多“原生态SQL查询”的信息,请参考EntityFramework团队的博客:Raw SQL Queries。
【“无跟踪”查询】
当数据库上下文示例从数据库中获取相关数据行并为他们的呈现创建对应实体类的时候,默认情况下无论内存中的实体与实际数据库中的数据是否同步都是被跟踪的。内存中的数据表现形如缓存,当你需要更新数据的时候它们便发生作用。不过实际上在Web程序中这种“缓存”并不是必要的,因为上下文的实体是“短周期存在”(每来一个新的请求,新的实体被创建了,随后自动被销毁……)。典型情况下,一个用于读取实体的上下文对象总是在它被使用前先销毁,后创建。
当然你可以手动指定上下文数据库对象是否跟踪——只需设置“AsNoTracking”属性即可。典型情况下你需要做类似设置的有:
1)查询获取了大量数据,如果关闭了跟踪状态的话性能明显得到提高。
2)你把一个实体类附加到上下文对象想做更新,但是早先之前你已经获得过该对象另作它用;因为这个对象已经被跟踪,所以你再也无法附加这个类。避免此类事件发生的办法之一就是在早先的查询中使用“AsNoTracking”。
这一节中你将使用上述描述的第二种方法实现业务逻辑——很明显地,你将指定一个规则:一个instructor(教师)不能是一个以上系的主管。
在“DepartmentController.cs”中,增加一个方法。该方法将被Edit和Create方法调用以确保任意两个系不会拥有同样的系管理员。
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); } } }
若验证数据合法并通过,在HttpPost方式下Edit中“Try”块下添加代码来调用上面的方法——现在的“Try”中代码看上去如下:
if (ModelState.IsValid) { ValidateOneAdministratorAssignmentPerInstructor(department); } if (ModelState.IsValid) { db.Entry(department).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); }
运行Department页中Edit部分,尝试为某个系指定一个已经是某个系的教员,你将得到如下异常信息:
现在请再一次运行Department中“Edit”页面,这次换成Budget,当点击“Save”时会弹出一个错误页面:
错误信息“An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.”可能是由于以下情况引发:
1)Edit调用“ValidateOneAdministratorAssignmentPerInstructor
”方法,这将获取所有包含“Kim Abercrombie”作为其系管理员的系,这样的话English(英语)系也被读取;那时因为那个系处于编辑状态,因此没有错误显示报告出来。作为读取的结果,目前“英语系”被数据库上下文进行了跟踪。
2)Edit方法试图修改由模型类绑定机制创建的“英语系”(English)实体类的状态标识符Modified,但是最终修改失败,是因为此对象已经被数据库上下文对象所跟踪。
解决此问题的一种方案就是使得数据库上下文对象避免跟踪由验证请求获取处于内存中的实体对象。不过这样做毫无优势可言——因为你不会再更新这个实体,或者因为“缓存在内存”中带来的某些优势而读取这个对象实体。
在“DepartmentController.cs”文件“ValidateOneAdministratorAssignmentPerInstructor”方法中,用以下代码设置“无需跟踪”状态:
var duplicateDepartment = db.Departments .Include("Administrator") .Where(d => d.PersonID == department.PersonID) .AsNoTracking() .FirstOrDefault();
重复之前的步骤(试图编辑一个系的Budget),这一次操作是成功的,页面也返回了预期的效果——一个被更新了的预算值。
【检查发送到数据库中的请求】
有时候对我们而言看一看真实发送到数据库中的SQL语句究竟是什么对我们有所帮助。欲达此目的,请在debugger检测该请求变量,或是使用请求的“ToString()”方法。为了尝试此效果,请看以下简单的查询语句,并注意观察如果你使用了诸如“饥饿模式”、“过滤”以及“排序”时候的状况:
在“Controllers/CourseController”中用以下代码替换Index中的代码:
public ViewResult Index() { var courses = unitOfWork.CourseRepository.Get(); return View(courses.ToList()); }
现在请在“GenericRepository.cs”的“return query.ToList();”语句上设置一个断点,并在Get方法“return orderBy(query).ToList();”上也设置一个断点,用“诊断(Debug)模式”运行该程序选择Course的Index页面——当代码运行至断点处检查变量“query”,你就可以看到完整发送到SQL Compact的语句了——这是一句简单的SQL语句:
{SELECT [Extent1].[CourseID] AS [CourseID], [Extent1].[Title] AS [Title], [Extent1].[Credits] AS [Credits], [Extent1].[DepartmentID] AS [DepartmentID] FROM [Course] AS [Extent1]}
有时语句可能因为太长而无法在Debug窗口中显示出来,此时为了看清,请拷贝这个变量中的内容到其它文本编辑器中:
现在你向Course的Index页面增加一个下拉列表以便我们可以根据一个特定的系进行数据筛选。你通过“标题”对课程进行排序,然后你对Department的导航属性使用“饥饿模式”进行数据加载,在“CourseController.cs”中请用下列代码进行替换:
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参数接受从下拉框中选定的数值,如未选中任何东西此变量将为空。
“SelectList”集合包含了所有传递给下拉列表的系,传递给SelectList的参数分别指定了“值名称”、“显示名称”以及“选中的值”。
对于Course存储类的Get方法,代码为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> }
保留刚才的所有断点再次运行Course的Index页面,一步步走过这些断点页面也就在浏览器中呈现出来;从下拉列表中选择一个系,单击Filter:
这一次第一处断点为下拉列表对系进行过滤,跳过那个步骤,下一次当代码到达断点之时请查看query变量中的内容——你将看到如下内容:
{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,同时加载了Departments和Course的数据。并且包含一个Where条件过滤设置语句。
【使用代理类工作】
当EntityFramework创建了实体实例的时候(比如当你执行一个命令),它常常生成动态衍生类实体,像是实体的代理一般;这个代理重写了实体的一些虚方法,这样当这些属性被访问之时,它就可以插入一些“钩子代码”自动执行一些命令了。打个比方来说,此机制用于支持“慢模式”关系数据中的加载。
大部分情况下你对此不比在意,以下情况例外:
1)在某些场合下如果你想避免让EntityFramework生成这些代理类实体(比如比起序列化代理类实体而言,序列化非代理类实体显然效率高)。
2)当你使用new操作符去实例化一个实体类的时候你不会得到一个代理实体,这意味着你无法获取诸如“慢模式”加载以及自动跟踪实体状态的功能,一般而言这毫无问题(因为你创建了一个新的实体,它不在数据库中自然也不需要“慢模式”加载;如果你把一个实体的状态显式地更改为Added,自然也不需要状态跟踪)。但是当你需要“慢模式”加载或是需要进行状态跟踪的时候,需要通过使用DbSet类中的Create方法创建具备这些衍生功能的类实体了。
3)你或许想从一个代理类型中获取一个真实的实体类型,你可以从ObjectContext类中使用其GetObjectType获取一个代理类型真实所代表的类型。
有关此更多细节信息,请参阅EntityFramework博客上的Working with Proxies。
【禁用“自动检测数据改变”机制】
EntityFramework可以自动侦测到数据是如何改变的(因此在做更新数据之前要把实体类的原始记录和当前记录进行对比),原始记录数据当实体被查询出或是被附加的时候存储了起来,一些方法可以导致改变自动检测机制:
DbSet.Find
DbSet.Local
DbSet.Remove
DbSet.Add
DbSet.Attach
DbContext.SaveChanges
DbContext.GetValidationErrors
DbContext.Entry
DbChangeTracker.Entries
如果你在跟踪大量实体的状态,并且在循环中使用到了如上方法中的一种的话,或许你借助“AutoDetectChangesEnabled”关闭自动状态检测机制来大幅度提升效率。有关于此信息你可以参考EntityFramework博客上“Automatically Detecting Changes”一文。
【保存是禁用验证机制】
当调用SaveChanges方法时,EntityFramework默认情况下总是先对所有已经变化的实体类中所有的属性值进行检测,如果你需要更新大批量的数据实体并且这些实体已经通过了验证,那么这一步显然多余,你可以通过暂时性地关闭验证机制使得保存数据的进度花费更少的时间(借助“”属性进行设置)。关于此更多的信息你可以参考EntityFramework博文中“Validation”一篇。
【有关与EntityFramework其它一些资料】
到此为止,EntityFramework系列已经全部结束。更多关于EntityFramework的资料您可以参考:
- Introduction to the Entity Framework 4.1 (Code First)
- The Entity Framework Code First Class Library API Reference
- Entity Framework FAQ
- The Entity Framework Team Blog
- Entity Framework in the MSDN Library
- Entity Framework in the MSDN Data Developer Center
- Entity Framework Forums on MSDN
- Julie Lerman's blog
- Code First DataAnnotations Attributes
- Maximizing Performance with the Entity Framework in an ASP.NET Web Application
- Profiling Database Activity in the Entity Framework
- Entity Framework Power Tools
以下在EntityFramework博客上的文章提供了本系列教程涉及到的一些技术的更多细节:
- Fluent API Samples. How to customize mapping using fluent API method calls.
- Connections and Models. How to connect to different types of databases.
- Pluggable Conventions. How to change conventions.
- Finding Entities. How to use the
Find
method with composite keys. - Loading Related Entities. Additional options for eager, lazy, and explicit loading.
- Load and AsNoTracking. More on explicit loading.
列举在这里的许多文章都是和EntityFramework(CTP5)相关的,大部分内容都是精确的,不过在CTP5和官方发布的Code-First最终版本之间或许会有一些变化。
有关更多EntityFramework+LINQ的结合使用,请参考“LINQ to Entities”。
有关更多EntityFramework+MVC的结合使用,请参考“ MVC Music Store ”。
有关更多发布已编译的网站应用程序,请参考MSDN库中“ASP.NET Deployment Content Map”。