英文渣水平,大伙凑合着看吧……html
这是微软官方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表格中显示集合中的内容。mvc
在Student控制器中,Details视图中的动做方法使用Find方法来检索单个学生实体。app
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参数传递给方法用于检索。框架
<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
<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导航属性时,一个新查询发送到数据库以检索数据。您能够在这里阅读更多关于延迟加载和预先加载的相关信息。)
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无需任何更改。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }
在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方法从新显示带错误消息的提示页面,给用户一个机会,取消或重试。
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而且错误信息被传递给视图。
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。
<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平台及工具团队的高级程序员,做家。