EF6与mvc5系列(2):实现基本的增删查改

在上节中添加控制器以后,项目中自动生成了增删改查视图。本节中我们将对其进行修改。

注意:在实际开发中,通常会在数据库和数据访问层之间使用仓库模式来创建抽象层,由于本系列教程我们主要讲EF,所有不会涉及这些。有关信息可查看:ASP.NET Data Access Content Map.

我们可以看到mvc自动创建了一些视图页(Create,Delete,Detail)。

创建详情页

我们打开Student控制器中的Detail方法,如下:

 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参数,并且它来自Index页面里的Detail超链接里的路由数据(route data)。这里说下route data

route data

默认的路由格式为:controller,action 和ID.在下面的URL中,控制器为Instructor,Action为Index,id是1。这就是路由数据的值。

http://localhost:1230/Instructor/Index/1?courseID=2021

其中"?courseID=2021"。是一个查询字符串的值。如果你将id作为查询字符串的值,也可以,如下。

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

在Razor视图中,ActionLink创建了url,下面代码中,id与默认路由匹配,所以,id被添加到路由数据中。

 @Html.ActionLink("Select", "Index", new { id = item.PersonID  })

然而下面的代码中,在默认路由中没有与CourseID参数相匹配的,所以它就会作为一个查询字符串加入。

@Html.ActionLink("Select", "Index", new { courseID = item.CourseID }) 

虽然项目中有Detail视图,但是我们无法查看学生的成绩信息,index视图忽略了Enrollments属性。所以修改Detail视图页如下,添加Enrollments信息。

 1 @model ContosoUniversity.Models.Student
 2 
 3 @{
 4     ViewBag.Title = "Details";
 5 }
 6 
 7 <h2>Details</h2>
 8 
 9 <div>
10     <h4>Student</h4>
11     <hr />
12     <dl class="dl-horizontal">
13         <dt>
14             @Html.DisplayNameFor(model => model.LastName)
15         </dt>
16 
17         <dd>
18             @Html.DisplayFor(model => model.LastName)
19         </dd>
20 
21         <dt>
22             @Html.DisplayNameFor(model => model.FirstMidName)
23         </dt>
24 
25         <dd>
26             @Html.DisplayFor(model => model.FirstMidName)
27         </dd>
28 
29         <dt>
30             @Html.DisplayNameFor(model => model.EnrollmentDate)
31         </dt>
32 
33         <dd>
34             @Html.DisplayFor(model => model.EnrollmentDate)
35         </dd>
36         <dt>
37             @Html.DisplayNameFor(model=>model.Enrollments);
38         </dt>
39         <dd>
40             <table class="table">
41                 <tr>
42                     <th>Course Title</th>
43                     <th>Grade</th>
44                 </tr>
45                 @foreach(var item in Model.Enrollments)
46                 {
47                     <tr>
48                         <td>
49                             @Html.DisplayFor(modelItem=>item.Course.Title)
50                         </td>
51                         <td>
52                             @Html.DisplayFor(modelItem=>item.Grade);
53                         </td>
54                     </tr>
55                 }
56             </table>
57         </dd>
58     </dl>
59 </div>
60 <p>
61     @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
62     @Html.ActionLink("Back to List", "Index")
63 </p>

ctrl+k+d格式化下代码。ok!

上述代码遍历了Enrollments导航属性中的实体。每一个Enrollment实体,显示出了Course的Title和Grade。通过检索Enrollments实体中的Course导航属性中存储的Enrollments实体,从而查询到Course的Title值。当需要这些数据的时候就会自动执行查询。换句话说,此处使用了懒加载,无需为Course的导航属性指定预加载,所以在查询学生信息的时候,不会查询Enrollment信息。然而,当第一次连接到Enrollments的导航属性的时候,会发送一个查询请求到数据库。有关更多懒加载(lazy loading)和预加载(eager loading)请查看Reading Related Data。

运行效果如下:

Detail页面完成。

修改Create页面

修改控制器中HttpPost特性修饰的Create方法:添加try catch语句

        public ActionResult Create([Bind(Include="LastName, FirstMidName, EnrollmentDate")] Student student)
        {
            try
            {
//服务器端验证,稍后将会看到客户端验证
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { ModelState.AddModelError("","创建失败!"); } return View(student); }

从上述代码Bind属性中移除ID,是因为ID是主键,向数据库中插入数据的时候,会自动创建主键,无需我们自己添加。

安全提示ValidateAntiForgeryToken属性可以防止跨站点请求伪造攻击。它需在视图中有相应的Html.AntiForgeryToken()声明,稍后将会看到。

Bind属性可以让我们在创建的时候,可以向数据库中写入那些字段。比如,在Student实体中,有个Secret属性,但是在向数据库中写入实体的时候,我们不希望在页面传递Secret的值到数据库。

如果Student实体包含一个Secret属性,但你不想在页面设置这个字段,即使你页面上没有这字段,黑客也可以通过工具( fiddler)或者写一些JavaScript传递这个字段值。如果没有模型绑定的Bind属性,黑客就可以添加Secret字段到你的实体中。你可以通过Bind的Include参数设置白名单,也可以通过Exclude设置黑名单。使用Include会更安全,因为当你给实体添加一个新的属性的时候,这个字段不会自动被包含在黑名单中。

为了防止在页面输入你不想传入数据库中的字段,在编辑的时候,读取第一次访问数据库中得到的实体信息,然后调用TryUpdateModel。指定被允许传入的属性列表。

另一种方法也就是现在很多开发者使用的方法:使用视图模型而不是模型绑定的实体类。在视图模型中仅包含你想要修改的属性。一旦MVC模型绑定完成,复制视图模型属性到实体实例。此步骤可以使用工具例如: AutoMapper。使用实体实例的db.Entry设置它的状态为Unchanged,然后对包含在视图模型中的每一个实体属性,设置属性名.IsModified为true。

除了Bind,还添加了try-catch语句块。这样当创建数据出现错误的时候,会抛出“创建失败”的异常。而不会抛出代码错误。在实际项目中我们会将这些异常写入日志中,更多信息请查看: Monitoring and Telemetry (Building Real-World Cloud Apps with Azure).

Create.chstml页面中也包含@Html.AntiForgeryToken()。这会与controller中的ValidateAntiForgeryToken一起,防止跨站点请求伪造攻击。(prevent cross-site request forgery attacks.)

在Create页面创建信息的时候,如果输入的日期格式不正确,页面会提示日期无效可见开启了客户端验证,后面章节中,我们会学习客户端验证的特性。

Create页面完成。

修改HttpPost修饰的Edit方法

Student控制器中有两个Edit方法,这里我们只修改带有HttpPost的Edit方法。

 [HttpPost,ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public ActionResult EditPost(int ?id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            var studentToUpdate = db.Students.Find(id);            
            if (TryUpdateModel(studentToUpdate,"",new string []{ "LastName", "FirstMidName", "EnrollmentDate"}))
            {
                try
                {
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
                catch (DataException)
                {
                    ModelState.AddModelError("","修改失败");
                }
            }
            return View(studentToUpdate);
        }

自动生成的代码我们不推荐使用。因为在使用Bind属性的时候,如果有些字段没有写在Include中,Bind就会清除掉这些之前就已经存在的数据。以后mvc自动生成的Edit方法中的代码会修改为不在包含Bind属性。

上述代码中,读取实体信息,然后调用TryUpdateModel将实体信息修改为用户的输入信息然后通过表单post到数据库。EF会自动跟踪然后为实体设置Modified 标志。当SaveChanges被调用的时候,Modified标志会使EF创建sql语句修改数据库中的数据。这里可以忽略并发冲突,并且数据库中的所有数据都被修改,包括用户没有修改的实体。后面的教程中我们将会学习如何处理并发冲突,如果你只是想修改数据库中的一些字段,可以设置属性为Unchanged,或者设置你的字段为Modifed。

为了防止在修改的时候,在页面输某些你不想保存到数据库的字段(overposting)。可以在Edit页面,将你想修改的字段放在TryUpdateModel参数中。

我们修改了HttpPost修饰的Edit方法,为了便于区分,所以将方法更名为EditPost。

实体状态,Attach和Savechanges方法

我们写的的数据库上下文类,可以跟踪到内存中的实体是否与数据库的数据内容同步。是否同步决定了当你调用Savechanges方法将会发生什么。例如:当你向Add方法中添加一个实体的时候,这个实体的状态就会被设置成Added,然后你调用Savechanges方法,数据库上下文就会发出一个sql的INSERT命令。

实体可能是如下状态:

Added:数据库中不存在该实体,Savechanges方法会发出一个Insert声明。

UnChanged:Savechanges方法不会对该实体做任何处理,当你从数据库中读取一个实体的时候,就是以这种状态开始的。

Modified:实体的一些或者全部属性值被修改后,Savechanges方法会发出一个Update声明

Deleted:实体已经被标记为删除,Savechanges方法会发出一个Delete声明。

Detached:实体未被数据上下文跟踪。

在桌面应用程序中,通常都是自动设置状态变化,读取实体并修改其属性值,这会将实体状态自动设置成Modified,然后调用Savechanges方法,EF会生成Update语句仅修改你想修改的属性值。

断开连接的web app不会发生以上这些连续操作。页面呈现出以后DbContext会读取显示的实体信息,调用HttpPost修饰的Edit方法时,会生成一个新请求和一个新的DbContext实例。因此你必须手动设置实体状态为Modified。当调用Savechanges方法时,EF修改了数据库中的所有行,因为上下文无法知道你修改的是哪个属性。

如果你想让sql 的update语句只修改你要修改的属性值。可以用一些方法保留原始值(例如隐藏字段)。可以使用原始值创建实体并调用Attach方法。修改实体值为新值然后调用Savechanges方法。有关信息请查看:Entity states and SaveChangesLocal Data

Edit页面完成。

修改Delete页面

同前面一样,delete操作也有两个方法,Get请求的方法会展现一个视图给用户确定是否删除,如果用户删除,那么就执行Post请求。当post请求执行后才会真正删除数据。

Get请求的delete方法中,通过Find方法找到实体,这里对这个稍作修改,如果查找实体失败,给与提示用户。

在post请求的delete方法中,添加try-catch。

        public ActionResult Delete(int? id,bool? saveChangesError=false)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            if (saveChangesError.GetValueOrDefault())
            {
                ViewBag.ErrorMessage = "删除失败!";
            }
            Student student = db.Students.Find(id);
            if (student == null)
            {
                return HttpNotFound();
            }
            return View(student);
        }
// POST: /Student/Delete/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Delete(int id)
        {
            try
            {
                Student studentToDelete = new Student() {ID=id };
                db.Entry(studentToDelete).State = EntityState.Deleted;
                db.SaveChanges();
            }
            catch (DataException)
            {
                return RedirectToAction("Delete", new { id = id, saveChangesError = true });
            }
            return  RedirectToAction("Index");
        }

当选择了一要删除的实体后,上述代码就会在数据库中查找,然后调用Remove方法将实体状态设置为Deleted。当调用SaveChanges方法的时候就会生成一个delete的sql语句。我们将方法改名由原来自动生成的DeleteConfirmed 改为为Delete,修改之前自动生成的代码中,将HttpPost的删除方法命名为DeleteConfirmed是为了给HttpPost方法一个特殊的签名。现在我们这里用到了重载,httppost和httpget方法名就可以相同了。

为了提高程序性能,避免不必要的sql查询检索数据。可以将上述代码中的Find和remove方法替换为如下。

Student studentToDelete = new Student() { ID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;

使用主键声明一个实体,然后设置该实体的状态为 Deleted,

修改Delete.cshtml视图代码,添加ViewBag

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

Delete功能完成!

关闭数据库连接

关闭连接尽可能快的释放资源。当我们不使用的时候要销毁上下文实例。代码提供在控制器最后提供了一个Dispose方法,基础的Controller类已经实现了IDisposable接口,因此只需重写Dispose(bool)方法销毁数据库上下文实例即可。

 protected override void Dispose(bool disposing)
 {
     db.Dispose();
     base.Dispose(disposing);
 }

 

事务处理

当你修改多行数据或者表,然后执行Savechanges方法的时候,EF会默认自动执行事务。EF自动确保你所做更改要么全部成功,要么全部失败。如果在修改一系列数据的过程中,前面所有的更改会自动回滚。有关事务请查看: Working with Transactions

 

posted @ 2016-03-28 13:02  天空的一片云  阅读(539)  评论(0编辑  收藏  举报