英文渣水平,大伙凑合着看吧……
这是微软官方SignalR 2.0教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第二篇:实现基本的CRUD功能
原文:Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application
译文版权所有,谢绝全文转载——但你可以在你的网站上添加到该教程的链接。
在之前的教程中,我们使用实体框架及SQL Server LocalDB创建了一个用来存储和显示数据的MVC应用程序。在本教程中,你将审阅并定义 MVC脚手架在控制器和视图中自动为您创建的CRUD(创建、读取、更新、删除)代码。
注意:通常我们实现仓储模式,即在你的控制器和数据存取层之间创建一个抽象层来存取数据。为了保持教程的简洁并将注意力聚焦在如何使用实体框架上,我们在本教程中没有使用仓储模式。关于更多的信息,请参阅ASP.NET Data Access Content Map。
在本教程中,你将建立以下Web页面:
创建一个详细页面
在学生索引页面中,脚手架代码将Enrollments属性排除在外,因为该属性是一个集合。在详细页面中,我们将在HTML表格中显示集合中的内容。
在Student控制器中,Details视图中的动作方法使用Find方法来检索单个学生实体。
1 public ActionResult Details(int? id) 2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 Student student = db.Students.Find(id); 8 if (student == null) 9 { 10 return HttpNotFound(); 11 } 12 return View(student); 13 }
索引页上详细信息超链接的路由数据中的键值以id参数传递给方法用于检索。
- 打开Details视图,每个字段都使用DisplayFor帮助器来呈现数据,如下面的代码所示:
<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
- 在EnrollmentData字段后,</dl>标签之前,将下列高亮代码添加到显示列表中:
<dd> @Html.DisplayFor(model => model.EnrollmentDate) </dd> <dt> @Html.DisplayNameFor(model => model.Enrollments) </dt> <dd> <table class="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> </dd> </dl> </div>
如果代码缩进有问题,你可以在粘贴过代码后按下Ctrl-K-D来纠正它。
此段代码遍历Enrollments导航属性中的实体,在遍历到的每个Enrollment实体中,显示出课程标题和成绩。课程标题从Enrollments实体下的Course导航属性中的Course实体中获取。所有这些数据是在需要时自动从数据库检索到的。(换句话说,在此处您正在使用延迟加载。你没有指定Courses导航属性是需要预先加载的,所以在同一次查询中,只有学生的数据从数据库中查询并读取。相反,当您第一次尝试访问Enrollments导航属性时,一个新查询发送到数据库以检索数据。您可以在这里阅读更多关于延迟加载和预先加载的相关信息。) - 运行项目,点击学生选项卡并点击Alexander的详情连接。(如果你按下Ctrl+F5时,Details.cshtml文件时打开的,你会收到一个HTTP 400错误页面。因为VS会认为你想要查看Details页面而直接打开该页面。由于不是从连接点击进入该页面,所以页面无法获得所需的参数而发生错误。在这种情况下,你需要从URL移除Student/Details然后重试。或者关闭浏览器,右键点击项目,点击视图,然后点击在浏览器中查看。)
你可以看到你选择学生的课程及成绩。
更新创建页面
- 在Student控制器中,使用以下的代码添加一个try-catch代码块并从Bind特性中删除ID属性来替换脚手架生成的HttpPost创建方法。
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Create([Bind(Include="LastName,FirstMidName,EnrollmentDate")] Student student) 4 { 5 try 6 { 7 if (ModelState.IsValid) 8 { 9 db.Students.Add(student); 10 db.SaveChanges(); 11 return RedirectToAction("Index"); 12 } 13 } 14 catch (DataException) 15 { 16 ModelState.AddModelError("", "保存数据时出现错误。请重试,如果问题依旧存在请联系系统管理员。"); 17 } 18 return View(student); 19 }
这段代码将ASP.NET MVC模型绑定器创建的Student实体添加到学生实体集合并保存到数据库中。(模型绑定器是能够使你更轻松地处理表单提交数据的ASP.NET MVC功能;模型绑定器将提交的表单值转换为CLR值并将它们传递给动作方法中的参数。在本例中,模型绑定器使用了Form表单集合中的值来实例化了一个学生实体。)
因为ID是主键值,在插入新纪录时,SQL Server会自动设置该值,所以我们将ID从Bind特性中删除来禁止用户设置该值。
安全注意事项:ValidateAntiForgeryToken属性有助于防止跨站请求伪造攻击,它需要在视图中相应地设置Html.AntiForgeryToken()语句,您将在后面看到。
Bind特性用于防止“过多发布”攻击。举例来说,假设Student实体中包含一个Secert字段,你不想让此属性由Web页面来进行更新,所以你没有在页面上放置Secert的相应输入框。但黑客可以通过工具强行附加Secert字段即相应值到表单中并发送给服务器端。在没有使用Bind的默认情况下,模型绑定器会自动遍历提交过来的所有表单值并尝试更新到实体中,所以Secert也会得到更新——使用黑客强行附加的值。
安全的做法是使用Bind特性的Include参数,可以让你指定那些字段是由模型绑定器来进行更新的,也可以相反地使用Exclude来排除你不想让模型绑定器来进行更新的属性。我们推荐使用Include的理由是,如果对实体添加了新的属性,Exclude是不会自动更新的,新属性会默认被模型绑定器进行更新。
另一种替代方法是使用ViewModel。ViewModel中仅包含你想要绑定的属性。在模型绑定器完成对ViewModel的更新后,将ViewModel中的属性复制到实体的实例已完成更新。
try-catch块是除了Bind特性外您对脚手架代码所做的唯一更改。如果在保存时有一个源于DataException的异常被引发,一个通用的错误消息被显示出来。由于DataException错误有时会由外部的应用程序引发,而不是程序编写的错误,所以建议用户进行再次尝试。此外,虽然该实例中没有实现,在生产环境下,所有的应用程序错误都应该被记录下来。
Create.cshtml的代码类似Details.cshtml的,除了DisplayFor被EditorFor和ValidationMessageFor帮助器替代了。下面是相关的代码:<div class="form-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div> </div>
Create.cshtml还包含@Html.AntiForgeryToken()方法和控制器中的ValidateAntiForgeryToken特性,已防止跨站请求伪造攻击。
Create.cshtml无需任何更改。 - 通过选择学生选项卡,单击新创建运行该页面。
- 输入姓名和无效的日期,然后单击Create查看错误消息。
这是默认情况下的服务器段验证。在以后的教程中您会看到如何添加特性并生成客户端验证。以下突出显示的代码显示了创建方法中的模型验证检查。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
- 将日期更改为一个有效的值,单击创建,然后参阅索引页面中显示的新学生。
更新编辑HttpPost页面
在Student控制器中,HttpGet Edit方法(没有HttpPost特性的那一个)使用Find方法来检索所选择的Student实体,正如你在Details方法中看到的一样。您不需要更新此方法。
使用以下代码以添加一个try-catch块来替换HttpPost Edit方法:
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Edit([Bind(Include="ID,LastName,FirstMidName,EnrollmentDate")] Student student) 4 { 5 try 6 { 7 if (ModelState.IsValid) 8 { 9 db.Entry(student).State = EntityState.Modified; 10 db.SaveChanges(); 11 return RedirectToAction("Index"); 12 } 13 } 14 catch (DataException) 15 { 16 ModelState.AddModelError("","无法保存变更,请重试,如果问题依旧,请联系管理员。") 17 } 18 return View(student); 19 }
这段代码类似于你在HttpPost Create方法中看到的那样,但不是将由模型绑定器创建的提示添加到实体集,这段代码设置实体上的标志位,表明它已经被更改。当调用SaveChanges方法时,Modified标志使实体框架来创建SQL语句并执行以更新数据库。数据库中该行的所有列都将被更新,包括哪些用户没有改变的,并发冲突被忽略。
实体状态和附加和调用SaveChanges方法
数据库上下文会跟踪内存中的实体是否与数据库中的行保持同步。并根据同步的信息来确定调用SaveChanges方法时会发生什么。例如,让你传递一个新实体给Add方法,该实体的状态设置为Added。然后您调用SaveChanges方法时,数据库上下文会生成一个SQL Insert命令以插入数据。
一个实体可能处于以下状态之一:
- Added。该实体尚未在数据库中。SaveChanges方法将发出一个Insert语句。
- Unchanged。SaveChanges对该实体什么都不需要做。当你从数据库读出一个实体时,该实体就为这一状态。
- Modified。某些或所有实体的属性值已都被更改。SaveChanges将发出一个Update语句。
- Deleted。该实体已经被标志为删除。SaveChanges将发出一个Delete语句。
- Detached。该实体没有被跟踪的数据库上下文。
在桌面应用程序中,状态变化通常是自动设置的。在桌面型的应用程序中,你看到一个实体并更改它的一些属性值,将导致它的实体状态自动更改为Modified。然后你调用SaveChanges,实体框架生成一个SQL Update来更新你进行了变更的属性。
Web应用程序的断开连接性质不允许这种连续序列。数据库上下文在读取到实体并将其呈现在页面上,之后便被销毁。当HttpPost Edit动作方法被调用时,一个新请求被处理,你将获取一个新的数据库上下文的实例。所以你必须手动设置实体状态为Modified,然后你调用SaveChanges,实体框架更新数据库中的所有的数据行,因为上下文没有办法知道那个属性是你进行了变更的。
如果你想在SQL Update语句只更新用户实际更改的字段,你可以以某种方式保存原来的值(比如隐藏字段),这样在调用HttpPost Edit方法时就可以使用它们。然后,你可以使用原值来创建一个Student实体,调用原始版本的Attach方法更新实体的值到新值,然后调用SaveChanges。更多信息请参见MSDN上的Entity states and SaveChanges 和 Local Data。
如同你在Create.cshtml中见到的一样,Edit.cshtml中的HTML和Razor代码无需更改。
通过选择学生选项卡,单击一个学生的编辑超链接运行该页面。
改变一些数据并单击保存,你可以在索引页面中看到你所做出的更改。
更新删除页面
在学生控制器中,HttpGet Delete方法的模板代码使用Find方法检索所选的Student实体,正如你在Details和Edit方法中看到的那样。然而,调用SaveChanges失败时的错误信息需要修正,你需要向该方法和视图中添加一些功能。
类似你之前看到的更新和创建操作,删除操作需要两个动作方法。Get请求用来显示一个视图,让用户有机会批准或取消删除操作。如果用户批准,POST请求被创建,HttpPost Delete方法被调用,然后该方法将实际执行删除操作。
您将添加一个try-catch块到HttpPost Delete方法来处理数据库更新时可能发生的任何错误。如果出现了错误,则HttpPost Delete方法调用HttpGet Delete方法,向其传递一个参数表明发生了错误。HttpGet Delete方法重新显示带错误消息的提示页面,给用户一个机会,取消或重试。
- 使用下面的代码更新HttpGet Delete方法:
1 public ActionResult Delete(int? id,bool? saveChangesError = false) 2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 if (saveChangesError.GetValueOrDefault()) 8 { 9 ViewBag.ErrorMessage = "删除错误,请重试。如果错误依旧,请联系管理员。"; 10 } 11 Student student = db.Students.Find(id); 12 if (student == null) 13 { 14 return HttpNotFound(); 15 } 16 return View(student); 17 }
此代码接受一个可选择参数,指示该方法是否是由保存更改后出现了故障的的方法调用的。在HttpGet Delete方法不是由之前出现了错误的方法被调用的,该参数为false。当HttpPost Delete出现了错误,参数为true并且错误信息被传递给视图。
- 使用下面的代码替换HttpPost Delete动作方法(名称为DeleteConfirmed的那个)用来执行删除操作并捕获任何数据库更新错误。
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public ActionResult Delete(int id) 4 { 5 try 6 { 7 Student student = db.Students.Find(id); 8 db.Students.Remove(student); 9 db.SaveChanges(); 10 } 11 catch(DataException) 12 { 13 return RedirectToAction("Delete", new { id = id, saveChangesError = true }); 14 } 15 return RedirectToAction("Index"); 16 }
这段代码从数据库中检索要删除的实体,然后调用Remove方法将实体的状态设置为Deleted。当调用SaveChanges命令时,数据库上下文将生成SQL Delete命令将实体从数据库中删除。此外,我们将动作方法从DeleteConfirmed改为Delete。脚手架代码将DeleteConfirmed方法命名为HttpPost Delete动作以设置一个唯一的签名(CLR需要有不同的方法参数来重载方法)。现在,方法签名是唯一的,基于MVC的约定在可以让你通过使用HttpPost和HttpGet特性来使用相同名称的删除方法。
如果在一个高容量应用程序中改善性能是优先事项,你可以避免不必须要的SQL查询,使用下面的代码替换Find和Remove方法:
Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
这段代码使用唯一的一个主键值实例化了一个学生实体,然后将实体状态设置为Deleted。这便是实体框架删除一个实体所需要的全部信息。
要注意HttpGet Delete方法不会执行数据删除。在一个Get请求响应中执行删除动作(或者创建、修改等对数据进行变更的动作)将带来安全风险。有关风险的详细信息请参见ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes。 - 在Delete.cshtml中,在H2和H3标签之间添加错误信息,如下面的代码:
<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
运行程序,点击学生选项卡,点击某个学生的删除连接:
- 点击删除按钮,你会看到在索引页面中指定的学生已经被删除。(稍后的教程中我们将介绍并发处理)
确保数据库连接不是一直打开
要确保数据库连接正确的关闭并释放所占用的资源,当你使用完数据库上下问候,需要将其销毁。这就是为什么脚手架代码在Student控制器类的最后部分提供了一个Dispose方法,如下面的代码:
protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); }
控制器基类已经实现了IDisposeable接口,所以这段代码只是简单的重写了Dispose(bool)方法以显式地销毁上下文实例。
处理事务
默认情况下,实体框架隐式的实现事务处理。当你对多个表或行进行了更改后调用SaveChanges,实体框架会自动确保你的所有更改全部成功保存到数据库或全部保存失败。如果某些更新完成,之后发生了一个错误,那之前完成的更新将自动全部回滚。当你需要对事务的更多的控制权时——比如您想要在一次事务中包含在实体框架之外的操作——参见MSDN上的Working with Transactions。
总结
您现在拥有一套针对Student实体完成的CRUD操作。你使用了MVC帮助器来生成数据字段的UI元素,关于帮助器的更多信息,参见 Rendering a Form Using HTML Helpers。
在下一节教程中我们会给索引页添加排序和分页等更多的功能。
作者信息
Tom Dykstra - Tom Dykstra是微软Web平台及工具团队的高级程序员,作家。