近期学习MVC5+EF6,找到了Microsoft的原文,一个非常棒的系列,Getting Started with Entity Framework 6 Code First using MVC 5,网址:http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application。
这个系列的原文,可以上面网址找到。我也从网上找到了相关的译文。申明,这些译文,我不是原创,而是从网上找来的,为避免下次还要网上查询,现将这些译文整理放在上面。以后找时间将原文地址附上。
MVC5+EF6--2 实现基本的CRUD功能
Contoso University示例网站演示如何使用Entity Framework 5创建ASP.NET MVC 4应用程序。Entity Framework有三种处理数据的方式: Database First, Model First, and Code First. 本指南使用代码优先。其它方式请查询资料。示例程序是为Contoso University建立一个网站。功能包括:学生管理、课程创建、教师分配。本系列指南逐步讲述如何实现这一网站程序。
本示例程序基于 ASP.NET MVC.如果使用 ASP.NET Web Forms model, 请查看 Model Binding and Web Forms系列指南和 ASP.NET Data Access Content Map.
如有问题,可在这些讨论区提问: ASP.NET Entity Framework forum, the Entity Framework and LINQ to Entities forum, or StackOverflow.com.
在上一篇指南中创建了一个MVC程序,使用EF和SQL Server LocalDB存储、展示数据。这一指南回顾并自定义CRUD代码,之前的代码是MVC结构自动创建的控制器和视图代码.
Note 通常的做法是在控制器和数据访问层之间使用仓库模式创建一个抽象层。这里暂时不实现这一功能,后续再进行补充。请查看 (Implementing the Repository and Unit of Work Patterns).
本指南将创建如下页面:
创建详情页面
Index
页面的scaffolded代码不包括 Enrollments
属性, 因为其是集合. 在Details
页面将在 HTML 表中列出此集合的内容.
在Controllers\StudentController.cs, Details
视图的行为方法使用 Find 检索获取一个 Student
实体.
public ActionResult Details(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
主键的值通过地址路由参数传递
- 打开 Views\Student\Details.cshtml. 每个字段通过
DisplayFor
显示: 在EnrollmentDate
之后,在fieldset标记结束之前
, 添加如下代码显示注册信息列表:
2. <div class="display-label">
3. @Html.DisplayNameFor(model => model.LastName)
4. </div>
5. <div class="display-field">
6. @Html.DisplayFor(model => model.LastName)
</div>
8. <div class="display-label">
9. @Html.LabelFor(model => model.Enrollments)
10. </div>
11. <div class="display-field">
12. <table>
13. <tr>
14. <th>Course Title</th>
15. <th>Grade</th>
16. </tr>
17. @foreach (var item in Model.Enrollments)
18. {
19. <tr>
20. <td>
21. @Html.DisplayFor(modelItem => item.Course.Title)
22. </td>
23. <td>
24. @Html.DisplayFor(modelItem => item.Grade)
25. </td>
26. </tr>
27. }
28. </table>
29. </div>
30. </fieldset>
31. <p>
32. @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
33. @Html.ActionLink("Back to List", "Index")
</p>
循环显示Enrollments导航属性中的每一个Enrollment实体,其中显示的课程名是通过Enrollment中的导航实体Course找到的。所有数据在需要时从数据库自动检索。也就是说,这里使用了延迟加载。无需添加代码,在第一次使用此属性时,数据从数据库中检索得到。随后在 Reading Related Data 进一步介绍延迟加载
- 点击列表中某一学生,即可看到详情信息如下:
更新Create页面
- 在Controllers\StudentController.cs, 替换
HttpPost
Create
方法的代码,添加try-catch
和 Bind attribute:
2. [HttpPost]
3. [ValidateAntiForgeryToken]
4. public ActionResult Create(
5. [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
6. Student student)
7. {
8. try
9. {
10. if (ModelState.IsValid)
11. {
12. db.Students.Add(student);
13. db.SaveChanges();
14. return RedirectToAction("Index");
15. }
16. }
17. catch (DataException /* dex */)
18. {
19. //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
20. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
21. }
22. return View(student);
}
代码将ASP.NET MVC模型绑定器生成的 Student
实体添加到 Students
实体集并保存到数据库. (模型绑定器指 ASP.NET MVC 的一项功能:使你更容易处理表单提交的数据; 模型绑定器将提交的表单数据转为 CLR 类型并作为参数传递到行为方法.本例中, 模型绑定器生成一个Student
实体,属性值来自于 Form
集合.)
ValidateAntiForgeryToken
特性阻止 cross-site request forgery 攻击.
安全提示: Bind
特性避免over-posting. 例如, 如果 Student
实体包含 Secret
属性,此属性不想通过此页面更新.
public class Student
{
public int StudentID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
即便本页面没有Secret
输入区域, 黑客可使用工具如 asfiddler, 或者JavaScript, 提交一个 Secret
表单值. 没有 Bind 特性对区域的限制,模型绑定器将在创建Student使用此表单值,黑客提交的内容将存入数据库. 下图显示通过工具添加Secret
区域(with the value "OverPost") 并提交表单信息.
虽然你没打算通过页面更新此属性,"OverPost" 将成功的把 Secret
属性的值添加到数据中.
安全的做法是使用Bind
特性的 Include
参数添加白名单.也可以使用 Exclude
添加你想排除的黑名单. 使用 Include
是因为更安全,当实体添加一个新属性, Exclude
列表不会自动包含此属性.
还有一种方法,很多人喜欢用,在模型绑定时使用视图模型. 视图模型只包含想绑定的属性. 一旦 MVC 绑定器完成工作,你再将视图模型中的属性值赋给实体对象.
除了Bind
特性, try-catch
是对默认代码做的另一改变. 如果异常来自 DataException , 将显示一个错误提示信息. DataException 异常可能来自于其它方面而非编程错误, 因此用户可尝试再次执行. 本指南没有涉及的内容是: 使用如 ELMAH产品质量程序记录错误日志.
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.chstml 也包括 @Html.AntiForgeryToken()
, 其和控制器的 ValidateAntiForgeryToken
特性一同阻止cross-site request forgery 攻击.
无需改变 Create.cshtml.
- 运行程序选择创建学生.
数据验证默认在起作用. 输入姓名和错误的日期点击创建,会看到提示信息.
此例中你看到JavaScript 实现的 通过客户端验证 (9月只有30天, 因此日期无效). 但服务器端得验证实现了 .即便客户端验证不起作用, 也能捕获坏的数据 (模型将无效),将转向Create
视图. 你可以通过禁用浏览器的JavaScript测试一下. 下面高亮代码显示了有效性判断.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Student student)
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(student);
}
将日期改成有效值,如 9/1/2005 点击创建,则此学生信息将加入到Index的学生列表.
更新 Edit POST页面
在 Controllers\StudentController.cs, HttpGet
Edit
方法(没有HttpPost
特性的那个方法) 使用 Find
方法检索到 Student
实体, 如在 Details
方法所见. 无需修改代码.
但 HttpPost
Edit
行为方法的代码需要添加 try-catch
和
Bind 特性:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
此代码和 HttpPost
Create
方法代码很像. 但是, 模型绑定器创建的对象不是添加到实体集, 而是通过设置flag告诉实体集,此实体发生了变化.当 SaveChanges 被调用, Modified flag 使得 Entity Framework 创建更新数据行的 SQL语句. 数据行的所有列都将更新,包括没有改变的那些列, 忽略并发冲突. 在 Handling Concurrency 将学习如何处理兵法冲突.)
实体状态、依附和 SaveChanges 方法
数据库上下文负责跟踪内存中的实体和数据库中的数据是否同步, 这将决定执行 SaveChanges
方法时的工作. 例如, 如果像 Add 方法传递了一个新的实体, 该实体状态将被设置为 Added
. 当调用SaveChanges 方法,数据库上下文执行 SQL INSERT
命令.
实体可能处于以下 状态:
Added
. 数据库中不存在.SaveChanges
方法执行INSERT
.Unchanged
. 执行SaveChanges
时什么也不用做. 当从数据库刚被读取出来时,实体为此状态.Modified
. 实体的某些属性值被改变.SaveChanges
执行UPDATE
.Deleted
. 实体被标记为删除.SaveChanges
执行DELETE
.Detached
. 实体未被数据库上下文跟踪.
在桌面程序,状态的改变是自动发生的. 在桌面程序 读取一个实体并修改其属性值. 实体状态自动设置为Modified. 当调用 SaveChanges
, Entity Framework 生成 SQL UPDATE
语句只更新发生改变的属性值.
连接断开是web程序的特点. DbContext 读取数据后页面呈现完毕即被释放. 当 HttpPost
Edit
行为方法被调用时, 产生新的请求生成新的 DbContext实例, 因此你必须设置实体状态为 Modified.
然后当调用 SaveChanges
, Entity Framework 更新数据行的所有列,因为数据上下文不知道哪些列的值发生变化.
如果希望 SQL Update
语句只更新变化了的数据列,可通过一些方法保存原始值 (如 hidden fields) 当调用 HttpPost
Edit
方法时可使用这些值. 然后使用这些原始值生成 Student
实体, 调用原有实体的 Attach
方法
, 然后调用 SaveChanges.
更多信息请查看 Entity states and SaveChanges 和Local Data .
Views\Student\Edit.cshtml 的代码和 Create.cshtml代码相似, 无需改变.
运行查看效果.
修改一些值,然后保存,Index将看到新的值.
更新Delete 页面
在Controllers\StudentController.cs, HttpGet
Delete
方法使用 Find
检索选中的 Student
实体, 如你在 Details
和Edit
方法所见一样. 但是, 当 SaveChanges
执行失败时如果需要显示错误提示,需要向方法和相应的视图添加一些功能.
如你在update 和create 操作所见, delete 操作需要两个行为方法. GET 提供用户查看详情和取消删除的功能 . 如果用户确认删除, 则触发POST. HttpPost
Delete
方法被调用删除将被执行.
请为HttpPost
Delete
添加 try-catch
处理可能由于数据库引起的异常. 如果出现异常, HttpPost
Delete
调用 HttpGet
Delete
方法, 向其传递一个表明错误的参数. HttpGet Delete
方法将附带错误信息重新显示删除确认页面, 让用户选中再试一次或者取消.
-
HttpGet
Delete
代码如下:
2. public ActionResult Delete(bool? saveChangesError=false, int id = 0)
3. {
4. if (saveChangesError.GetValueOrDefault())
5. {
6. ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
7. }
8. Student student = db.Students.Find(id);
9. if (student == null)
10. {
11. return HttpNotFound();
12. }
13. return View(student);
}
代码接受一个 optional 布尔参数,此参数标明是否附带错误信息. HttpGet
Delete
不包括错误信息时此参数值为false
. 当被 HttpPost
Delete
调用时, 此参数为true
并向视图传递错误信息.
-
HttpPost
Delete
代码如下:
15. [HttpPost]
16. [ValidateAntiForgeryToken]
17. public ActionResult Delete(int id)
18. {
19. try
20. {
21. Student student = db.Students.Find(id);
22. db.Students.Remove(student);
23. db.SaveChanges();
24. }
25. catch (DataException/* dex */)
26. {
27. // uncomment dex and log error.
28. return RedirectToAction("Delete", new { id = id, saveChangesError = true });
29. }
30. return RedirectToAction("Index");
}
代码检索选中的实体, 调用 Remove 方法将实体状态设为 Deleted
. 当SaveChanges
执行时, 生成SQL DELETE
命令. 方法名由 DeleteConfirmed
改为Delete
. 自动生成的代码相应 HttpPost
Delete
的是DeleteConfirmed
方法,该方法被设置了HttpPost
. ( The CLR 要求重载的方法要有不同的参数.) 既然方法签名(参数)已经改变,可对删除的 HttpPost
和HttpGet
使用相同的方法名.
改进性能对于一个大量数据的程序来说很有必要, 使用下面的代码替换调用Find
和Remove
方法的代码,避免执行一次不必要的对数据的 SQL 查询:
Student studentToDelete = new Student() { StudentID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
代码仅使用主键初始化一个 Student
实体,并将该实体状态设为Deleted
. 这就是 Entity Framework删除实体所需要的.
如前面提到的, HttpGet
Delete
没有删除数据
. 通过 GET 请求执行删除操作(或者编辑、创建等其它引起数据变化的操作) 会引起风险. 更多信息请查看 ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes .
- 在 Views\Student\Delete.cshtml,在
h2
和h3
之间添加错误信息提示:
32. <h2>Delete</h2>
33. <p class="error">@ViewBag.ErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
运行程序:
- 点击删除 Index页面将显示删除后的学生列表. (在随后的 Handling Concurrency 中将看到异常的情况.)
确保关闭了数据库连接
为了确保数据库连接关闭而且由此占用的资源也被释放, 请确定释放了数据上下文的实例. 这是自动生成的StudentController
类代码包含 Dispose 方法的原因, 如下所示:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
Controller
基类已经实现了 IDisposable
接口, 此代码只是简单的添加了对 Dispose(bool)
方法的重载以释放数据上下文实例.
总结
已经创建了一系列的页面实现对Student
实体的 CRUD 操作. 使用 MVC 帮助器生成数据域的HTML代码. 更多有关 MVC helpers的信息, 请查看 Rendering a Form Using HTML Helpers (the page is for MVC 3 but is still relevant for MVC 4).
下一节将通过添加排序和分页扩展Index页面的功能.
其它 Entity Framework相关资源请查看 ASP.NET Data Access Content Map.