1. 程式人生 > >MVC5 Entity Framework學習之實現基本的CRUD功能

MVC5 Entity Framework學習之實現基本的CRUD功能

在上一篇文章中,我們使用Entity Framework 和SQL Server LocalDB建立了一個MVC應用程式,並使用它來儲存和顯示資料。在這篇文章中,你將對由 MVC框架自動建立的CRUD(create, read, update, delete)程式碼進行修改。

注意:通常我們在控制器和資料訪問層之間建立一個抽象層來實現倉儲模式,為了將注意力聚焦在如何使用實體框架上,這裡暫沒有使用倉儲模式。

在本篇文章中,要建立的web頁面:



1.建立一個Details頁面

由框架程式碼生成的Students Index頁面暫沒有考慮Enrollments屬性,因為該屬性是一個集合。在Details頁面中,我們將在HTML表格中顯示集合中的內容。

開啟 Controllers\StudentController.cs,可以看到對應Details檢視的Details方法使用Find方法來檢索單個學生實體:

public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

Details方法的id引數來自Index頁面中Details連結,稱為路由資料(route data)。

路由資料是指在路由表中指定,通過URL傳遞,由模型繫結器接收的資料。如下所示,預設路由指定了controller, action和 id

 routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
在下面的 URL中,預設路由將Instructor 對映為controller, Index對映為action, 1 對映為 id
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引數 不匹配預設路由,所以courseID被作為查詢字串
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID }) 


開啟Views\Student\Details.cshtml,每個欄位都使用DisplayFor幫助器來顯示資料,如下面的程式碼所示:

<dt>
    @Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
    @Html.DisplayFor(model => model.LastName)
</dd>

在EnrollmentData欄位之後,</dl>標籤之前,新增下面的程式碼
        <dt>
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>

        <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>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

如果程式碼縮排不正確,可以使用Ctrl-K-D快捷鍵來糾正它。 

上面的程式碼遍歷Enrollments導航屬性中的實體,對於每一個Enrollment實體,顯示出Course Title 和Grade。Course Title是從Enrollments實體中的Course導航屬性中的Course實體中獲取的,所有這些資料豆是在需要時自動從資料庫檢索的。(換句話說,這裡使用的是延遲載入。你沒有為Courses導航屬性指定預先載入,所以在同一次查詢中,只檢索了Students資料而沒有檢索enrollments資料。相反,在第一次試圖訪問Enrollments導航屬性時,會建立一個新的查詢併發送到資料庫。

執行專案,選擇Students 選項卡並點選名為Alexander Carson的Details 連結。(如果你按Ctrl+F5,直接開啟Details.cshtml,會得到HTTP 400錯誤,因為Visual Studio會直接開啟Details頁面卻沒有指定任何一個studen,路由匹配錯誤導致程式出錯。在這種情況下,你只需要從URL中刪除Student/Details然後重試)

可以看到所選學生的courses 和grades


2.更新Create 頁面

開啟Controllers\StudentController.cs,使用下面的程式碼替換HttpPost Create方法

[HttpPost]
        [ValidateAntiForgeryToken]
        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)
            {
                //Log the error (uncomment dex variable name 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);
        }

上面的程式碼將ASP.NET MVC模型繫結器建立的Student實體新增到Students 實體集並儲存到資料庫中。(模型繫結器可以讓你更容易的提交表單資料,可以將提交的表單值轉換為CLR值並將它們作為引數傳遞給Controller中的方法。在本專案中,模型繫結器使用了表單集合中的屬性值例項化了一個Student 實體)

這裡刪除了Bind 屬性中的ID引數,因為ID是primary key,SQL Server在插入資料時會自動設定該值。

安全注意:ValidateAntiForgeryToken屬性有助於防止跨站請求偽造(cross-site request forgery)攻擊,但是需要在檢視中設定相應的Html.AntiForgeryToken()語句。

Bind屬性可以防止過份提交(over-posting)。舉例來說,假設Student實體中包含一個Secret 欄位,你不希望在Web頁面中更新它

 public class Student
   {
      public int ID { 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; }
   }

即使在Web頁面中沒有Secret欄位,黑客也可以通過工具例如Fiddler或者JavaScript 將表單資料包括Secret值提交到伺服器。如果不使用Bind屬性來限制模型綁器需要的欄位,模型繫結器會將接收到的Secret值更新至資料庫中,下面的截圖是通過Fiddler工具來提交表單資料




OverPost值將會被成功的更新至資料庫,這是你不希望看到的。

為了安全起見,最好使用Bind屬性的Include引數,也可以使用Exclude引數排除那些你不想要更新的屬性。但是這裡推薦使用Include,因為如果你在實體中添加了一個新的屬性,Exclude並不會將這個新新增的屬性排除在外。

另一種替代方法是在模型繫結時使用檢視模型,檢視模型中只包含你想要繫結的屬性。

除了Bind屬性,上面的程式碼中只需要加入try-catch塊,如果在儲存更改時引發DataException異常,就會在頁面中顯示相應的錯誤資訊。DataException異常有時是由外部事件引發而不是因為程式錯誤,所以建議使用者重試。記住在生產環境下,所有的應用程式錯誤都應該被記錄下來。

Views\Student\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()方法以防止跨站請求偽造攻擊。

執行專案,選擇Students選項卡,並點選Create New

輸入姓名和無效的日期,然後單擊Create檢視錯誤訊息


預設情況下使用的是伺服器端驗證,以後會教大家通過新增屬性來生成客戶端驗證,下面的程式碼展示了Create 方法中的模型驗證檢查

if (ModelState.IsValid)
{
    db.Students.Add(student);
    db.SaveChanges();
    return RedirectToAction("Index");
}

修改日期為一個有效的值,點選Create,可以看到新新增的Student資訊


3.更新Edit HttpPost頁面

在Controllers\StudentController.cs中,HttpGet Edit方法(沒有使用HttpPost屬性的那一個)和Details方法一樣使用Find方法來檢索所選擇的Student實體。

使用下面的程式碼替換HttpPost Edit方法:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include = "ID,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 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方法,但不同的是這裡在實體中設定了一個標誌位來指明它已經被更改,而不是將由模型繫結器建立的實體新增到實體集。當呼叫SaveChanges方法時,Modified標誌會導致 Entity Framework建立SQL語句並更新資料庫。資料庫中該行的所有列都將被更新,包括那些使用者並沒有更改的,並忽略併發衝突。

實體狀態、附加和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來更新資料庫。

DbContext 在讀取一個實體並將其呈現到頁面上後就會被銷燬,當HttpPost Edit方法被呼叫,此時會生成一個新的請求和DbContext 例項,所以你必須手動設定實體狀態為Modified,然後當你呼叫SaveChanges方法時,Entity Framework 會更新資料庫行的所有列,因為資料庫上下文沒有辦法知道你到底更改了哪些屬性。

如果你希望SQL Update語句只更新那些使用者實際更改的欄位,你可以先將原來的值以某種方法(比如隱藏欄位)儲存起來,這樣在呼叫HttpPost Edit方法時就可以使用它們,然後你可以使用原來的值來建立一個Student實體,呼叫Attach方法,並使用新的值更新該實體,最後呼叫SaveChanges方法。

Views\Student\Edit.cshtml 中的HTML 和Razor程式碼與Create.cshtml中的很類似。

執行專案,選擇Students選項卡,點選其中一個學生的Edit連結


修改其中的值,點選Save,可以在Index頁面中看到已經修改過的資料


4.更新Delete頁面

在Controllers\StudentController.cs中,由模板生成的HttpGet Delete方法使用Find方法檢索所選的Student實體。然而,當呼叫SaveChanges方法失敗時為了顯示自定義的錯誤資訊,你需要向該方法和相對應的檢視中新增一些功能。

就像update和create操作,delete操作也需要兩個動作方法。用於響應Get請求的方法用來顯示一個可以讓使用者<批准或取消delete操作的檢視,如果用確認執行delete操作,此時會產生一條POST請求,並呼叫HttpPost Delete方法,該方法執行真正的delete操作。

在HttpPost Delete方法中新增try-catch塊可以用來捕獲資料庫更新時可能出現的任何錯誤,如果出現了錯誤,則HttpPost Delete方法會呼叫HttpGet Delete方法,並向其傳遞一個引數指明發生了錯誤,然後HttpGet Delete會顯示一個錯誤資訊,並給使用者一個取消或重試的機會。

使用下面的程式碼替換HttpGet Delete方法:

public ActionResult Delete(int? id, bool? saveChangesError=false)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    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)
    {
        return HttpNotFound();
    }
    return View(student);
}

上面的程式碼接受一個可選擇引數,指明該方法在儲存更改出現錯誤後是否被呼叫。當HttpGet Delete方法不是由於出現錯誤而被呼叫的話,該引數值為false,當HttpPost Delete出現了錯誤而呼叫HttpGet Delete方法時該引數為true並在相應的檢視上顯示錯誤資訊。

使用下面的程式碼替換HttpPost Delete方法(名稱為DeleteConfirmed的那個),此方法用來執行真正的delete操作並捕獲任何資料庫更新錯誤

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
    try
    {
        Student student = db.Students.Find(id);
        db.Students.Remove(student);
        db.SaveChanges();
    }
    catch (DataException/* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.
        return RedirectToAction("Delete", new { id = id, saveChangesError = true });
    }
    return RedirectToAction("Index");
}

上面的程式碼從資料庫中檢索要刪除的實體,然後呼叫Remove方法將實體的狀態設定為Deleted,最後呼叫SaveChanges方法並生成一條SQL Delete命令。另外你也可以將方法名DeleteConfirmed改為Delete。框架程式碼將HttpPost Delete方法命名為DeleteConfirmed是為了為其設定一個獨一無二的名稱(CLR過載方法需要有不同的引數)。現在遵守MVC的約定,HttpPost和HttpGet delete方法使用了相同的名字,併為它們設定不同的引數。

如果你想提高高訪問量應用程式的效能,你要避免使用不必要的SQL查詢。使用下面的程式碼替換Find和Remove方法

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

上面的程式碼使用唯一的主鍵值例項化了一個學生實體並設定實體狀態為Deleted,這便是Entity Framework為了刪除一個實體所需要做的動作。

如前所述HttpGet Delete方法並不會執行資料刪除操作,在一個Get請求響應中執行delete操作(執行任何edit操作、create操作或者其它對資料進行更改的操作)將帶來安全風險。

在Views\Student\Delete.cshtml中新增錯誤資訊

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

執行專案,點選Students選項卡,點選其中一個學生的Delete連結:


點選Delete,你會看到在Index頁面中該學生已經被刪除。

5.確保資料庫連線適時關閉

要確保資料庫連線被正確的關閉並釋放所佔用的資源,在你使用完資料庫上下文時,必須要將其銷燬,這就是為什麼框架程式碼在StudentController.cs的最後部分提供了一個Dispose方法

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

Controller類實現了IDisposeable介面,所以上面的程式碼通過重寫Dispose(bool)方法來顯式的銷燬資料庫上下文例項。

6.處理事務

預設情況下,Entity Framework隱式的實現事務處理。當你對多行或者多個表進行更改後呼叫SaveChanges方法,Entity Framework會自動確保所有更改要麼全部成功要麼全部失敗。如果已經做完一些更改後發生了一個錯誤,那麼所有的更改包括已做完的都將自動回滾。

還大家一個健康的網路環境,從你我做起

THE END