MVC中使用EF(2):实现基本的CRUD功能

时间:2023-03-08 18:06:59
MVC中使用EF(2):实现基本的CRUD功能
MVC中使用EF(2):实现基本的CRUD功能
By  Tom Dykstra |July 30, 2013
Translated by litdwg

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 *.com.

在上一篇指南中创建了一个MVC程序,使用EF和SQL Server LocalDB存储、展示数据。这一指南回顾并自定义CRUD代码,之前的代码是MVC结构自动创建的控制器和视图代码.

Note  通常的做法是在控制器和数据访问层之间使用仓库模式创建一个抽象层。这里暂时不实现这一功能,后续再进行补充。请查看 (Implementing the Repository and Unit of Work Patterns).

本指南将创建如下页面:

MVC中使用EF(2):实现基本的CRUD功能

MVC中使用EF(2):实现基本的CRUD功能

MVC中使用EF(2):实现基本的CRUD功能

MVC中使用EF(2):实现基本的CRUD功能

创建详情页面

Index 页面的scaffolded代码不包括 Enrollments 属性, 因为其是集合. 在Details 页面将在 HTML 表中列出此集合的内容.


Controllers\StudentController.cs
,  
Details
 视图的行为方法使用  
Find
 检索获取一个 
Student
 实体.

publicActionResultDetails(int id =0){Student student = db.Students.Find(id);if(student ==null){returnHttpNotFound();}returnView(student);}

主键的值通过地址路由参数传递  

  1. 打开 Views\Student\Details.cshtml. 每个字段通过DisplayFor 显示:
    <divclass="display-label">
    @Html.DisplayNameFor(model => model.LastName)
    </div><divclass="display-field">
    @Html.DisplayFor(model => model.LastName)
    </div>
  2. 在EnrollmentDate 之后,在 fieldset标记结束之前, 添加如下代码显示注册信息列表:

    <divclass="display-label">
    @Html.LabelFor(model => model.Enrollments)
    </div><divclass="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></fieldset><p>
    @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
    @Html.ActionLink("Back to List", "Index")
    </p>

    循环显示Enrollments导航属性中的每一个Enrollment实体,其中显示的课程名是通过Enrollment中的导航实体Course找到的。所有数据在需要时从数据库自动检索。也就是说,这里使用了延迟加载。无需添加代码,在第一次使用此属性时,数据从数据库中检索得到。随后在 Reading Related Data 进一步介绍延迟加载

  3. 点击列表中某一学生,即可看到详情信息如下:

    MVC中使用EF(2):实现基本的CRUD功能

更新Create页面

  1. Controllers\StudentController.cs, 替换 HttpPost Create  方法的代码,添加try-catch   Bind attribute:
    [HttpPost][ValidateAntiForgeryToken]publicActionResultCreate([Bind(Include="LastName, FirstMidName, EnrollmentDate")]Student student){try{if(ModelState.IsValid){
    db.Students.Add(student);
    db.SaveChanges();returnRedirectToAction("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.");}returnView(student);}

    代码将ASP.NET MVC模型绑定器生成的 Student 实体添加到 Students 实体集并保存到数据库. (模型绑定器指 ASP.NET MVC 的一项功能:使你更容易处理表单提交的数据; 模型绑定器将提交的表单数据转为  CLR 类型并作为参数传递到行为方法.本例中, 模型绑定器生成一个Student 实体,属性值来自于 Form 集合.)

    ValidateAntiForgeryToken 特性阻止 cross-site request forgery 攻击.

    安全提示: Bind 特性避免over-posting. 例如, 如果 Student实体包含 Secret 属性,此属性不想通过此页面更新.

    publicclassStudent{publicintStudentID{get;set;}publicstringLastName{get;set;}publicstringFirstMidName{get;set;}publicDateTimeEnrollmentDate{get;set;}publicstringSecret{get;set;}publicvirtualICollection<Enrollment>Enrollments{get;set;}}

    即便本页面没有Secret 输入区域, 黑客可使用工具如 asfiddler, 或者JavaScript, 提交一个 Secret 表单值. 没有 Bind 特性对区域的限制,模型绑定器将在创建Student使用此表单值,黑客提交的内容将存入数据库. 下图显示通过工具添加Secret 区域(with the value "OverPost") 并提交表单信息.

    MVC中使用EF(2):实现基本的CRUD功能

    虽然你没打算通过页面更新此属性,"OverPost" 将成功的把 Secret 属性的值添加到数据中.

    安全的做法是使用Bind 特性的 Include 参数添加白名单.也可以使用 Exclude 添加你想排除的黑名单. 使用 Include是因为更安全,当实体添加一个新属性,  Exclude 列表不会自动包含此属性.

    还有一种方法,很多人喜欢用,在模型绑定时使用视图模型. 视图模型只包含想绑定的属性. 一旦 MVC 绑定器完成工作,你再将视图模型中的属性值赋给实体对象.

    除了Bind 特性, try-catch 是对默认代码做的另一改变. 如果异常来自 DataException , 将显示一个错误提示信息. DataException 异常可能来自于其它方面而非编程错误, 因此用户可尝试再次执行. 本指南没有涉及的内容是: 使用如 ELMAH产品质量程序记录错误日志.

    Views\Student\Create.cshtml 代码和Details.cshtml的代码相似, 除了使用 EditorFor 和ValidationMessageFor 帮助器而非 DisplayFor. 相关代码如下:

    <divclass="editor-label">
    @Html.LabelFor(model => model.LastName)
    </div><divclass="editor-field">
    @Html.EditorFor(model => model.LastName)
    @Html.ValidationMessageFor(model => model.LastName)
    </div>

    Create.chstml 也包括 @Html.AntiForgeryToken(), 其和控制器的 ValidateAntiForgeryToken特性一同阻止cross-site request forgery 攻击.

    无需改变 Create.cshtml.

  2. 运行程序选择创建学生.

    MVC中使用EF(2):实现基本的CRUD功能

    数据验证默认在起作用. 输入姓名和错误的日期点击创建,会看到提示信息.

    MVC中使用EF(2):实现基本的CRUD功能

    此例中你看到JavaScript 实现的 通过客户端验证 (9月只有30天, 因此日期无效). 但服务器端得验证实现了 .即便客户端验证不起作用, 也能捕获坏的数据 (模型将无效),将转向Create 视图. 你可以通过禁用浏览器的JavaScript测试一下. 下面高亮代码显示了有效性判断.

    [HttpPost][ValidateAntiForgeryToken]publicActionResultCreate(Student student){if(ModelState.IsValid){
    db.Students.Add(student);
    db.SaveChanges();returnRedirectToAction("Index");}returnView(student);}

    将日期改成有效值,如 9/1/2005 点击创建,则此学生信息将加入到Index的学生列表.

    MVC中使用EF(2):实现基本的CRUD功能

更新 Edit POST页面

在 Controllers\StudentController.cs, HttpGet Edit 方法(没有HttpPost 特性的那个方法) 使用 Find方法检索到 Student 实体, 如在 Details 方法所见. 无需修改代码.

但 HttpPost Edit 行为方法的代码需要添加 try-catchBind 特性:

[HttpPost][ValidateAntiForgeryToken]publicActionResultEdit([Bind(Include="StudentID, LastName, FirstMidName, EnrollmentDate")]Student student){try{if(ModelState.IsValid){
db.Entry(student).State=EntityState.Modified;
db.SaveChanges();returnRedirectToAction("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.");}returnView(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代码相似, 无需改变.

运行查看效果.

MVC中使用EF(2):实现基本的CRUD功能

修改一些值,然后保存,Index将看到新的值.

MVC中使用EF(2):实现基本的CRUD功能

更新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 方法将附带错误信息重新显示删除确认页面, 让用户选中再试一次或者取消.

  1. HttpGet Delete 代码如下:
    publicActionResultDelete(bool? saveChangesError=false,int id =0){if(saveChangesError.GetValueOrDefault()){ViewBag.ErrorMessage="Delete failed. Try again, and if the problem persists see your system administrator.";}Student student = db.Students.Find(id);if(student ==null){returnHttpNotFound();}returnView(student);}

    代码接受一个 optional 布尔参数,此参数标明是否附带错误信息.  HttpGet Delete 不包括错误信息时此参数值为false  . 当被 HttpPost Delete 调用时, 此参数为true 并向视图传递错误信息.

  2. HttpPost Delete 代码如下:

    [HttpPost][ValidateAntiForgeryToken]publicActionResultDelete(int id){try{Student student = db.Students.Find(id);
    db.Students.Remove(student);
    db.SaveChanges();}catch(DataException/* dex */){// uncomment dex and log error. returnRedirectToAction("Delete",new{ id = id, saveChangesError =true});}returnRedirectToAction("Index");}

    代码检索选中的实体, 调用 Remove 方法将实体状态设为 Deleted. 当SaveChanges 执行时, 生成SQL DELETE 命令. 方法名由 DeleteConfirmed 改为Delete. 自动生成的代码相应 HttpPost Delete的是DeleteConfirmed 方法,该方法被设置了HttpPost  . ( The CLR 要求重载的方法要有不同的参数.) 既然方法签名(参数)已经改变,可对删除的 HttpPost 和HttpGet 使用相同的方法名.

    改进性能对于一个大量数据的程序来说很有必要, 使用下面的代码替换调用Find 和Remove方法的代码,避免执行一次不必要的对数据的 SQL 查询:

    Student studentToDelete =newStudent(){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 .

  3. 在 Views\Student\Delete.cshtml,在h2 和 h3 之间添加错误信息提示:

    <h2>Delete</h2><pclass="error">@ViewBag.ErrorMessage</p><h3>Are you sure you want to delete this?</h3>

    运行程序:

    MVC中使用EF(2):实现基本的CRUD功能

  4. 点击删除 Index页面将显示删除后的学生列表. (在随后的 Handling Concurrency 中将看到异常的情况.)

确保关闭了数据库连接

为了确保数据库连接关闭而且由此占用的资源也被释放, 请确定释放了数据上下文的实例. 这是自动生成的StudentController 类代码包含 Dispose 方法的原因, 如下所示:

protectedoverridevoidDispose(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.