前のチュートリアルでは、Entity Framework (EF) 6 と SQL Server LocalDB を使ってデータを保存して表示する MVC アプリケーションを作成しました。 このチュートリアルでは、MVC スキャフォールディングがコントローラーとビュー用に自動的に作成する作成、読み取り、更新、削除 (CRUD) コードを確認およびカスタマイズします。
Note
コントローラーとデータ アクセス層の間に抽象化レイヤーを作成するためにリポジトリ パターンを実装することは、よく行われることです。 この一連のチュートリアルが複雑にならないようにし、EF 6 自体の使い方に集中できるように、チュートリアルではリポジトリは使われていません。 リポジトリを実装する方法については、「ASP.NET データ アクセス コンテンツ マップ」を参照してください。
作成する Web ページの例を次に示します。



このチュートリアルでは、次の作業を行いました。
- [詳細の作成] ページ
- [作成] ページを更新する
- HttpPost Edit メソッドを更新する
- [削除] ページを更新する
- データベース接続を閉じる
- トランザクションを処理する
前提条件
[詳細の作成] ページ
Students Index ページのスキャフォールディングされたコードでは、Enrollments プロパティが省略されています。これは、このプロパティがコレクションを保持しているためです。 Details ページでは、コレクションの内容を HTML テーブルで表示します。
Controllers\StudentController.cs に含まれる Details ビューのアクション メソッドでは、Find メソッドを使って 1 つの Student エンティティを取得しています。
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);
}
キー値は id パラメーターとしてメソッドに渡され、[Index] ページの Details ハイパーリンクのルート データから取得されます。
ヒント: ルート データ
ルート データは、ルーティング テーブルで指定された 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
URL は Razor ビューの ActionLink ステートメントによって作成されます。 次のコードでは、id パラメーターが既定のルートと一致するため、id がルート データに追加されます。
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
次のコードでは、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>次の例で示すように、
EnrollmentDateフィールドの後から、終了タグ</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、Ctrl+D キーを押して書式設定します。
このコードは、
Enrollmentsナビゲーション プロパティ内のエンティティをループ処理します。 プロパティ内のEnrollmentエンティティごとに、コースのタイトルとグレードが表示されます。 コース タイトルは、EnrollmentsエンティティのCourseナビゲーション プロパティに格納されているCourseエンティティから取得されます。 このデータはすべて、必要なときにデータベースから自動的に取得されます。 つまり、ここでは遅延読み込みを使用しています。Coursesナビゲーション プロパティの一括読み込みを指定していないため、学生を取得したのと同じクエリで登録が取得されませんでした。 代わりに、Enrollmentsナビゲーション プロパティに初めてアクセスしようとすると、新しいクエリがデータベースに送信され、データが取得されます。 遅延読み込みと一括読み込みの詳細については、このシリーズの後半の関連データの読み取りに関するチュートリアルを参照してください。プログラムを起動し (Ctrl+F5)、[学生] タブを選択して、Alexander Carson の [詳細] リンクをクリックして、[詳細] ページを開きます。 (Details.cshtml ファイルが開いているときに Ctrl+F5 キーを押すと、HTTP 400 エラーが発生します。これは、Visual Studio が [詳細] ページを実行しようとしたが、表示する学生を指定するリンクからアクセスできなかったためです。その場合は、URL から "Student/Details" を削除して再試行するか、ブラウザーを閉じてプロジェクトを右クリックし、[表示]>[ブラウザーで表示] をクリックします。)
選んだ受講者のコースとグレードの一覧が表示されます。
ブラウザーを閉じます。
[作成] ページを更新する
Controllers\StudentController.csで、HttpPostAttribute
Createアクション メソッドを次のコードに置き換えます。 次のコードは、try-catchブロックを追加し、スキャフォールディングされたメソッドの BindAttribute 属性からIDを削除します。[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 /* 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); }このコードは、ASP.NET MVC モデル バインダーによって作成された
StudentエンティティをStudentsエンティティ セットに追加した後、変更をデータベースに保存します。 モデル バインダーとは、フォームによって送信されたデータの操作を容易にする ASP.NET MVC の機能です。モデル バインダーは、ポストされたフォーム値を CLR 型に変換して、パラメーター内のアクション メソッドに渡します。 この例のモデル バインダーは、Formコレクションからのプロパティ値を使って、Studentエンティティを自動的にインスタンス化します。IDは行が挿入されるときに SQL Server によって自動的に設定される主キー値であるため、Bind 属性からIDを削除しました。 ユーザーからの入力によってID値が設定されることはありません。セキュリティ警告 -
ValidateAntiForgeryToken属性は、クロスサイト リクエスト フォージェリ攻撃を防ぐのに役立ちます。 ビューに対応するHtml.AntiForgeryToken()ステートメントが必要です。後で説明します。Bind属性は、作成シナリオでオーバーポスティング攻撃を防ぐための 1 つの方法です。 たとえば、Studentエンティティに、この Web ページで設定したくないSecretプロパティが含まれているものとします。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フォーム値をポストすることできます。 モデル バインダーがStudentインスタンスを作成するときに使うフィールドを制限する BindAttribute 属性がないと、モデル バインダーはそのSecretフォーム値を取得し、それを使ってStudentエンティティ インスタンスを作成します。 その場合、Secretフォーム フィールドに対してハッカーが指定した値はすべて、データベースで更新されます。 次の図は、ポストされたフォームの値に、値 "OverPost" が含まれるSecretフィールドを追加している fiddler ツールを示しています。![[Composer] タブを示すスクリーンショット。右上隅の Execute は赤で囲まれています。右下隅のシークレットは、ポストの上に等しい赤で囲まれています。](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image5.png)
値 "OverPost" は挿入される行の
Secretプロパティに正常に追加されますが、Web ページがそのプロパティを設定できることは意図したものではありません。IncludeパラメーターをBind属性と共に使用して、フィールドを明示的に一覧表示することをおすすめします。Excludeパラメーターを使用して、除外するフィールドをブロックすることもできます。 その理由Includeは、エンティティに新しいプロパティを追加しても、新しいフィールドがExcludeリストによって自動的に保護されないためです。最初にデータベースからエンティティを読み取り、
TryUpdateModelを呼び出して明示的に許可されたプロパティ リストを渡すことにより、編集シナリオでの過剰ポスティングを防ぐことができます。 これらのチュートリアルではその方法が使われています。多くの開発者に好まれている、過剰ポスティングを防ぐためのもう 1 つの方法は、エンティティ クラスではなくビュー モデルをモデル バインドで使うことです。 更新するプロパティのみをビュー モデルに含めます。 MVC モデル バインダーが終了したら、必要に応じて AutoMapper などのツールを使って、ビュー モデルのプロパティをエンティティ インスタンスにコピーします。 エンティティのインスタンスで db.Entry を使ってその状態を Unchanged に設定した後、ビュー モデルに含まれる各エンティティ プロパティで Property("PropertyName").IsModified を true に設定します。 この方法は、編集シナリオと作成シナリオの両方で利用できます。
Bind属性以外では、スキャフォールディングされたコードに対して行った変更はtry-catchブロックだけです。 変更を保存するときに、DataException から派生した例外がキャッチされた場合は、汎用的なエラー メッセージが表示されます。 DataException 例外は、プログラミング エラーではなくアプリケーション外の何かが原因で発生する場合があるので、再試行することをお勧めします。 このサンプルでは実装されていませんが、運用品質のアプリケーションでは例外をログに記録します。 詳細については、「Monitoring and Telemetry (Building Real-World Cloud Apps with Azure)」(監視とテレメトリ (Azure での実際のクラウド アプリの構築)) の「Log for insight」(洞察のためのログ) セクションをご覧ください。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 には、コントローラーの
ValidateAntiForgeryToken属性と連携してクロスサイト リクエスト フォージェリ攻撃を防ぐ@Html.AntiForgeryToken()も含まれています。Create.cshtml では変更は必要ありません。
プログラムを開始し、[学生] タブを選択し、[新規作成] をクリックして、ページを実行します。
名前と無効な日付を入力し、[Create] をクリックすると、エラー メッセージが表示されます。
これは、既定で取得するサーバー側の検証です。 後のチュートリアルでは、クライアント側検証用のコードを生成する属性を追加する方法について説明します。 次の強調表示されたコードは、Create メソッドでのモデル検証チェックの部分です。
if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); }日付を有効な値に変更し、 [Create] をクリックして、新しい学生が [Index] ページに表示されることを確認します。
ブラウザーを閉じます。
HttpPost Edit メソッドを更新する
HttpPostAttribute
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 /* 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(studentToUpdate); }Note
Controllers\StudentController.cs に含まれる
HttpGet Editメソッド (HttpPost属性がないもの) は、Findメソッドを使って、選ばれたStudentエンティティを取得します (Detailsメソッドと同様)。 このメソッドを変更する必要はありません。これらの変更は、オーバーポストを防ぐため のセキュリティのベスト プラクティスを実装します。スキャフォールディングによって
Bind属性が生成 され、モデル バインダーによって作成されたエンティティが Modified フラグを持つエンティティ セットに追加されました。Bind属性は、Includeパラメーターにリストされていないフィールドの既存のデータをクリアするため、このコードは推奨されなくなりました。 今後、MVC コントローラー スキャフォールディングは、Edit メソッドのBind属性を生成しないように更新される予定です。新しいコードは、既存のエンティティを読み取り、TryUpdateModel を呼び出して、ポストされたフォーム データのユーザー入力からフィールドを更新します。 Entity Framework の自動変更追跡では、エンティティに EntityState.Modified フラグが設定されます。 SaveChanges メソッドが呼び出されると、Modified フラグによって、Entity Framework はデータベースの行を更新する SQL ステートメントを作成します。 コンカレンシーの競合 は無視され、ユーザーが変更しなかった列を含め、データベース行のすべての列が更新されます。 (後のチュートリアルでは同時実行の競合を処理する方法を示します。データベース内の個々のフィールドのみを更新する場合は、エンティティを EntityState.Unchanged に設定し、個々のフィールドを EntityState.Modified に設定できます。)
オーバーポストを防ぐために、[編集] ページで更新可能にするフィールドは、
TryUpdateModelパラメーターに一覧表示されます。 現在、他に保護しているフィールドはありませんが、モデル バインダーでバインドしたいフィールドをリストに入れておくと、後でデータ モデルにフィールドを追加した場合に、ここでフィールドを明示的に追加するまで、自動的にフィールドを保護できます。これらの変更の結果として、HttpPost Edit メソッドのシグネチャは、HttpGet Edit メソッドと同じになります。したがって、EditPost メソッドの名前を変更してあります。
ヒント
エンティティの状態と Attach および SaveChanges メソッド
データベース コンテキストは、メモリ内のエンティティがデータベースの対応する行と同期しているかどうかを追跡しており、この情報により、
SaveChangesメソッドを呼び出したときの処理が決まります。 たとえば、新しいエンティティを Add メソッドに渡すと、そのエンティティの状態はAddedに設定されます。 その後、SaveChanges メソッドを呼び出すと、データベース コンテキストは SQL のINSERTコマンドを発行します。エンティティは、次のいずれかの状態になる可能性があります。
Added. エンティティはデータベースにまだ存在しません。SaveChangesメソッドはINSERTステートメントを発行する必要があります。Unchanged.SaveChangesメソッドはこのエンティティに対し何も行う必要はありません。 データベースからエンティティを読み取ると、エンティティはこの状態で開始します。Modified. エンティティのプロパティ値の一部またはすべてが変更されています。SaveChangesメソッドはUPDATEステートメントを発行する必要があります。Deleted. エンティティには削除のマークが付けられています。SaveChangesメソッドはDELETEステートメントを発行する必要があります。Detached. エンティティはデータベース コンテキストによって追跡されていません。
デスクトップ アプリケーションにおいて、通常、状態の変更は自動的に設定されます。 デスクトップ タイプのアプリケーションでは、エンティティを読み取って一部のプロパティの値を変更すると、 そのエンティティの状態は自動的に
Modifiedに変更されます。 その後、SaveChangesを呼び出すと、Entity Framework は、変更された実際のプロパティのみを更新する SQLUPDATEステートメントを生成します。Web アプリは切断されているため、この連続したシーケンスには対応できません。 エンティティを読み取る DbContext は、ページがレンダリングされた後に破棄されます。
HttpPostEditアクション メソッドが呼び出されると、新しい要求が行われ、DbContextの新しいインスタンスが作成されるため、エンティティの状態を手動でModified.に設定する必要があります。その後、SaveChangesを呼び出すと、コンテキストで変更したプロパティを知る方法がないため、Entity Framework はデータベース行のすべての列を更新します。SQL
Updateステートメントでユーザーが実際に変更したフィールドのみを更新する場合は、元の値 (非表示フィールドなど) を保存して、HttpPostEditメソッドが呼び出されたときに使用できるようにすることができます。 その後、元の値を使ってStudentエンティティを作成し、元のバージョンのエンティティでAttachメソッドを呼び出して、エンティティの値を新しい値に更新した後、SaveChanges.を呼び出すことができます。詳細については、「エンティティの状態と SaveChanges」および「ローカル データ」を参照してください。Views\Student\Edit.cshtml の HTML コードと Razor コードは、Create.cshtml で確認したコードと似ています。変更は必要ありません。
プログラムを開始し、[学生] タブを選択し、[編集] ハイパーリンクをクリックして、ページを実行します。
データをいくつか変更し、 [Save] をクリックします。 [Index] ページに、変更したデータが表示されます。
ブラウザーを閉じます。
[削除] ページを更新する
Controllers\StudentController.cs では、HttpGetAttribute Delete メソッドのテンプレート コードでは、DetailsメソッドとEdit メソッドで確認したように、Find メソッドを使用して選択したStudent エンティティを取得します。 ただし、SaveChanges の呼び出しが失敗したときのカスタム エラー メッセージを実装するには、何らかの機能とその対応するビューをこのメソッドに追加します。
更新および作成操作で見たように、削除操作にも 2 つのアクション メソッドが必要です。 GET 要求に応答して呼び出されるメソッドは、ユーザーが削除操作を承認またはキャンセルできるビューを表示します。 ユーザーが操作を承認すると、POST 要求が作成されます。 その場合、 HttpPost Delete メソッドが呼び出され、そのメソッドが実際に削除操作を実行します。
HttpPostAttribute Delete メソッドにtry-catch ブロックを追加して、データベースの更新時に発生する可能性のあるエラーを処理します。 エラーが発生した場合、 HttpPostAttribute Delete メソッドは HttpGetAttribute Delete メソッドを呼び出し、エラーが発生したことを示すパラメーターを渡します。 次に、 HttpGetAttribute Delete メソッドは、エラー メッセージと共に確認ページを再表示し、ユーザーにキャンセルまたは再試行の機会を提供します。
HttpGetAttribute
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); }このコードは、変更保存の失敗後にメソッドが呼び出されたかどうかを示す省略可能なパラメーターを受け取ります。 このパラメーターは、
HttpGetDeleteメソッドが以前のエラーなしで呼び出されたときにfalseされます。 データベース更新エラーに応答してHttpPostDeleteメソッドによって呼び出されると、パラメーターはtrueされ、エラー メッセージがビューに渡されます。HttpPostAttribute
Deleteアクション メソッド (DeleteConfirmedという名前) を、実際の削除操作を実行し、データベース更新エラーをキャッチする次のコードに置き換えます。[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メソッドに一意のシグネチャを与えるためにDeleteConfirmedHttpPostDeleteメソッドという名前のスキャフォールディングされたコード。 (CLR では、さまざまなメソッド パラメーターを持つために、オーバーロードされたメソッドを必要とします。)シグネチャが一意になっているので、MVC 規則に準拠し、HttpPostとHttpGet削除メソッドに同じ名前を利用できます。大規模なアプリケーションでのパフォーマンス向上が優先される場合は、
FindとRemoveメソッドを呼び出すコード行を次のコードに置き換えることで、行を取得するための不必要な SQL クエリが実行されないようにすることができます。Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;このコードは、主キーの値のみを使用して
Studentエンティティをインスタンス化し、エンティティの状態をDeletedに設定します。 エンティティを削除するために Entity Framework に必要なものは主キーの値だけです前に示したように、
HttpGetDeleteメソッドはデータを削除しません。 GET 要求の応答で削除操作を実行すると (さらに言えば、編集操作、作成操作、データを変更するその他のあらゆる操作を実行すると)、セキュリティ上のリスクが生じます。Views\Student\Delete.cshtml で、次の例に示すように、
h2見出しとh3見出しの間にエラー メッセージを追加します。<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>プログラムを開始し、[学生] タブを選択し、[削除] ハイパーリンクをクリックして、ページを実行します。
[削除 しますか?] というページで [削除] を選択します。
削除された学生を含まない [インデックス] ページが表示されます。 (アクションでのエラー処理コードの例は、コンカレンシーチュートリアルをご覧ください。)
データベース接続を閉じる
データベース接続を閉じ、保持しているリソースをできるだけ早く解放するには、コンテキスト インスタンスが終了したら破棄します。 そのため、スキャフォールディングされたコードは、次の例に示すように、StudentController.cs の StudentController クラスの末尾に Dispose メソッドを提供します。
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
基底 Controller クラスは既に IDisposable インターフェイスを実装しているため、このコードは、コンテキスト インスタンスを明示的に破棄するために Dispose(bool) メソッドにオーバーライドを追加するだけです。
トランザクションを処理する
既定では、Entity Framework はトランザクションを暗黙的に実装します。 複数の行またはテーブルを変更してから SaveChanges を呼び出すシナリオでは、Entity Framework によって自動的に、すべての変更が成功するか、またはすべての変更が失敗することが保証されます。 一部の変更が完了した後でエラーが発生した場合、それらの変更は自動的にロールバックされます。 たとえば、Entity Framework の外部で行われる操作をトランザクションに含めたい場合など、より詳細な制御が必要なシナリオについては、「トランザクションの操作」をご覧ください。
コードを取得する
その他のリソース
Student エンティティに対して簡単な CRUD 操作を実行するページの完全なセットができあがりました。 MVC ヘルパーを使用して、データ フィールドの UI 要素を生成しました。 MVC ヘルパーの詳細については、「HTML ヘルパーによるフォームのレンダリング」を参照してください (この生地は MVC 3 用ですが、MVC 5 にも引き続き関連しています)。
他の EF 6 リソースへのリンクは、「ASP.NET データ アクセス - 推奨リソース」にあります。
次のステップ
このチュートリアルでは、次の作業を行いました。
- [詳細の作成] ページ
- Create ページを更新した
- 更新された HttpPost Edit メソッド
- Delete ページを更新した
- データベース接続を閉じた
- 処理されたトランザクション
次の記事に進み、プロジェクトに並べ替え、フィルター処理、ページングを追加する方法について説明します。