【EntityFramework系列教程二,翻译】借助EntityFramework在ASP.NET MVC程序中完成增删改查操作
在上一章中您学习了如何使用EntityFramework和SQL Server Compact创建一个ASP.NET MVC程序存储展示数据,在本章中您将学习如何在控制器和视图中对自动生成的增删改查代码进行自定义。
注意:为了在您的控制器和数据访问层中创建一个抽象层,通常的做法是实现一个“库模式”。不过为了使得这些教程变得更加简单,暂且不必实现此模式,我们将在稍后的“实现单元工作库模式”中得以完成。
本章中您要创建以下一系列页面:
【创建一个详细信息页】
Index页面默认架构忽略了Enrollments属性,那是因为这是一个集合属性。在详细信息页中你将通过一个Html表展示此一系列内容。
在“Controllers\StudentController.cs”文件中,“详细信息”页行为方法类似以下代码:
public ViewResult Details(int id)
{
Student student = db.Students.Find(id);
return View(student);
}
此代码使用Find方法根据传入id的主键寻找唯一的Student实例。“id”来源于一个“详细信息”超链接的查询字符串。
打开“Views\Student\Details.cshtml”文件,每个字段通过“DisplayFor”呈现出来;如下所示:
<div class="display-label">LastName</div>
<div class="display-field">
@Html.DisplayFor(model => model.LastName)
</div>
为显示一系列的Enrollments信息,在“EnrollmentDate”字段后面加上以下代码,它紧挨着结束节点“fieldset”:
<div class="display-label">
@Html.LabelFor(model => model.Enrollments)
</div>
<div class="display-field">
<table>
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</div>
本代码循环遍历了Enrollments中所有的实体,为每一个Enrollment都输出了课程名称以及学分。课程名称从课程实体中获取,它是Enrollments集合的导航属性。当需要的时候此数据完全从数据库中被取出(换句话说,你在应用“慢模式”而不是为Courses指定“饥饿模式”;所以当你第一次访问此属性时此请求就被发往数据库中取出对应的数据。在本系列后几章中你可以阅读到更多关于“读取相关数据”的内容。
运行程序,点击Students标签页,点击Details超链接,您将看到一系列课程信息:
【创建“新建学生”页面】
请在“Controllers\StudentController.cs”文件中用以下代码替换HttpPost Create方法,并对架构增加一个try……catch块:
[HttpPost]
public ActionResult Create(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
此代码通过ASP.NET MVC模型绑定到的Students实体集增加了一个Student并且存储到数据库中(所谓“数据绑定”是指ASP.NET MVC的内置功能,它使得你处理页面提交的数据变得更简单;一个数据模型绑定自动把提交的页面数据自动转化成.NET对应的类型数据,并且把他们传给带参数的方法。在这种情况下,模型绑定通过“窗体参数集合”初始化了Student类)。
“try……catch……”块在自动生成的架构和此代码颇有特色——如果对数据的任意改变被保存,此继承自“DataException”的异常就被捕获,一个通用的异常就被显示出来;这些种类的异常往往不是本质性的程序错误,非编码问题;因此用户也被要求重试一次。你在“Views\Student\Create.cshtml”所看到的代码与Details.cshtml中的相似(除使用“EditorFor”和“ValidationMessageFor”而不是“DisplayFor”之外)。以下便是相关代码:
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
此外,Create.cshtml没有其它改变。运行页面,点击Students选项卡随后点击“创建”(Create)。
数据验证自动发挥作用——再次尝试输入姓名以及一个非法日期,然后点击“Create”:
此类情况你看到的是客户端验证,它有javascript得以实现;但是服务端验证同样也实现了——即便客户端验证失效,非法数据将被捕获,同时异常也将在服务端被抛出。
然后把日期修改成“9/1/2005”,点击“Create”,就可以看到新的学生被添加到了Index页。
【创建一个编辑页面】
在“Controllers\StudentController.cs”,HttpGet Edit方法(不带“HttpPost”属性的那个)使用Find方法获取选定的Student实体,就像你在Details页面中看到的,此不必做任何改变。
然而请用以下带有“try……catch”块的代码取代HttpPost Edit方法:
[HttpPost]
public ActionResult Edit(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
此代码与之前看到的HttpPost Create方法如出一辙,不过这代码设置了一个标识符告知此实体状态已经发生改变,并非直接往实体集中添加实体。当“SaveChanges”方法被调用时,改变的标识符将使得EntityFramework产生更新语句更新数据库,这一行所有的列都将被更新(包括实际上并未做任何变更的列数据,并且忽略了并发冲突——您将在后面的“处理冲突”章节中学习如何处理冲突)。
【实体状态、附加以及保存命令】
数据上下文对象不断记录着内存中的数据实体是否与对应行中的数据保持一致,这个消息决定了你调用SaveChanges之后将会发生点什么——举个例子来说:如果你把一个新的实体作为参数传给Add方法,此实体的状态被设置成Added;那么当你调用SaveChanges的时候,数据库实际上执行了一次插入命令。
一个实体可能处于以下几种状态中:
Added(新增)
实体在数据库中不存在, SaveChanges将执行Insert命令。Unchanged(原始状态)
执行SaveChanges不触发任何行为—— 这通常是您从数据库中读取时候的状态。Modified(已更改)
发生于某个实体的任意属性被修改之后,SaveChanges将执行更新命令。Deleted(删除)
实体被标记为“删除”状态,SaveChanges执行删除命令。Detached(独立实体)
此实体不属于任何数据上下文对象。
在桌面程序中,状态改变通常自动设置;此类型程序中你读取了一个实体对象并对其一些属性做了修改,这将导致此实体对象状态变更成“Modified”,那么当你调用SaveChanges方法,EntityFramework自动生成了更新语句,只更新你变更过的属性。
不过在网站程序中此流程被中断——那是因为数据库上下文对象一旦在重新加载时被销毁了,那么当HttpPost Edit被调用后这将成为一个新的请求结果,你也拥有一个新的数据实体;自然你只能人为设置其状态为“Modified”。这样当你调用SaveChanges之后,EntityFramework将更新该行的所有列,因为上下文并不知道你对哪些列做了修改。
如果你只想对某些列进行更新,你可以通过(诸如隐藏域)等方式存储原始数据,他们对于HttpPost Edit仍旧有效;然后你利用这些原始数据创造一个新的Student对象,用Attach方法把那个对象附加到数据上下文对象上,然后更新实体数据,最后调用SaveChanges即可。想了解有关更多关于此的信息,请参考“新增、附加以及实体状态”一文,以及“本地数据”一章。
在“Views\Student\Edit.cshtml”中与你在“Create.cshtml,”看到的类似,无需做任何修改。
运行程序,点击“Students”并且单击“Edit”超链接:
对一些数据进行修改,单击“Save”可以看到在Index页中显示变更后的数据:
【创建一个删除页】
在“”中,默认HttpGet Delete代码使用了Find方法获取了Student对象——正如你在Edit和Details页看到的一样。不过为了在调用SaveChanges时发生异常而自定义错误消息代码,你应该对此方法以及对应的页面增加一些额外的功能:
就像你看到的更新和创建方法操作一样——删除操作其实分成两步:被调用的方法将先返回一个页面以便让用户确认是否真要删除该对象。如得到确认,那么一个Post的请求将被创建,HttpPost Delete方法也就被调用——实际上是那个方法真正执行了删除操作。
你将为HttpPost Delete添加一个try……catch块以便处理当数据库更新时所发生的任意类型的错误。如果错误发生,那么HttpPost Delete方法将调用HttpGet Delete方法,传递一个参数告知其是否有错误发生——HttpGet Delete方法返回了一个确认删除页面,以便让用户对删除进行最终确认。
请用以下代码替换HttpGet Delete方法,它用于组织管理错误信息的预报:
public ActionResult Delete(int id, bool? saveChangesError)
{
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator.";
}
return View(db.Students.Find(id));
}
此代码接受一个布尔类型的参数,表明其在更新失败后是否调用;当HttpGet Delete方法被调用作为对页面请求的回应时,布尔值默认是null(或者是false)。当被HttpPost Delete调用时作为对数据库更新错误的回应,此布尔对象被设置成true,错误信息也就被显示出来。
用“DeleteConfirmed”替代“HttpPost Delete”方法,该方法实际上执行删除命令,并捕获期间发生的任何错误:
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
try
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
return RedirectToAction("Delete",
new System.Web.Routing.RouteValueDictionary {
{ "id", id },
{ "saveChangesError", true } });
}
return RedirectToAction("Index");
}
以上代码获取了指定的Student实体,然后调用了Remove方法设置实体状态为删除,那么当调用SaveChanges的时候,SQL delete命令就被执行了。
如果考虑高性能优先,那么你应该避免调用“无意义”的SQL而改用以下语句:
Student studentToDelete = new Student() { StudentID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
此代码用主键初始化了Student实体,并变更其状态为Deleted;对于EntityFramework中“删除”而言要做的就那么多……。
这里再次提醒您一句——HttpGet Delete只是对引发的Get操作的回应,并不会真正删除数据;考虑到执行编辑、创建或者其它改变数据的操作会多少带来风险……请参见Stephen Walther博客 ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes(英文)。
请在“”文件的h2和h3(二级、三级标题)中加上如下代码:
<p class="error">@ViewBag.ErrorMessage</p>
运行程序,点击Students选项卡,点击Delete超链接即可:
Index页将不显示已被删除的记录(有关处理冲突的问题,会在以后“冲突处理”章节中看到一些例子)。
【确保数据库上下文对象不总是出于打开状态】
为确保数据连接正常被关闭并且它们占用的资源被释放,你应该注意上下文对象被销毁;那也就是为什么你在StudentController末尾处看到Dispose方法,如下所示:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
基类Controller已经实现了IDisposible接口,所以这个代码只是重写了“Dispose(bool)”方法,从而显式地
销毁数据库上下文对象。
你现在已经有了完整的一系列处理Students增删改查的页面了,下一章你将继续学习如何扩展Index页支持分页和排序。
关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。