ASP.NET MVC アプリケーションでの Entity Framework での基本的な CRUD 機能の実装 (2/10)

作成者: Tom Dykstra

Contoso University サンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルをご覧ください。

Note

解決できない問題が発生した場合は、 完了した章をダウンロード して、問題を再現してみてください。 通常、コードを完成したコードと比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」を参照してください。

前のチュートリアルでは、Entity Framework と LocalDB を使用してデータを格納および表示する MVC アプリケーションSQL Server作成しました。 このチュートリアルでは、MVC スキャフォールディングによってコントローラーとビューで自動的に作成される CRUD (作成、読み取り、更新、削除) コードを確認してカスタマイズします。

Note

コントローラーとデータ アクセス層の間に抽象化レイヤーを作成するためにリポジトリ パターンを実装することは、よく行われることです。 これらのチュートリアルをシンプルにするために、このシリーズの後のチュートリアルまでリポジトリを実装しません。

このチュートリアルでは、次の Web ページを作成します。

Contoso University Student Details ページを示すスクリーンショット。

Contoso University Student Edit ページを示すスクリーンショット。

Contoso University Student Create ページを示すスクリーンショット。

[学生の削除] ページを示すスクリーンショット。

詳細ページの作成

Students Index ページのスキャフォールディングされたコードは、そのプロパティがコレクションを Enrollments 保持しているため、プロパティを省略しました。 ページでは Details 、コレクションの内容を HTML テーブルに表示します。

Controllers\StudentController.cs では、ビューのDetailsアクション メソッドは メソッドをFind使用して 1 つのStudentエンティティを取得します。

public ActionResult Details(int id = 0)
{
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

キー値は パラメーターとして id メソッドに渡され、[インデックス] ページの [詳細 ] ハイパーリンクのルート データから取得されます。

  1. Views\Student\Details.cshtml を開きます。 次の例に示すように、各フィールドはヘルパーを使用して DisplayFor 表示されます。

    <div class="display-label">
             @Html.DisplayNameFor(model => model.LastName)
        </div>
        <div class="display-field">
            @Html.DisplayFor(model => model.LastName)
        </div>
    
  2. フィールドの EnrollmentDate 後と終了 fieldset タグの直前に、次の例に示すように、登録の一覧を表示するコードを追加します。

    <div class="display-label">
            @Html.LabelFor(model => model.Enrollments)
        </div>
        <div class="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 、コースのタイトルと成績が表示されます。 コースタイトルは、エンティティのCourseナビゲーション プロパティEnrollmentsCourse格納されているエンティティから取得されます。 このデータはすべて、必要なときにデータベースから自動的に取得されます。 (つまり、ここでは遅延読み込みを使用しています。ナビゲーション プロパティの一括読み込みをCourses指定しなかったため、そのプロパティに初めてアクセスしようとすると、データを取得するためのクエリがデータベースに送信されます。遅延読み込みと一括読み込みの詳細については、このシリーズの後半の「関連データの読み取り」チュートリアルを参照してください)。

  3. ページを実行するには、[ 学生 ] タブを選択し、Alexander Carson の [詳細 ] リンクをクリックします。 選んだ受講者のコースとグレードの一覧が表示されます。

    Student_Details_page

作成ページの更新

  1. Controllers\StudentController.cs で、action メソッドをHttpPost``Create次のコードに置き換えて、 ブロックと Bind 属性をスキャフォールディング メソッドに追加try-catchします。

    [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 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);
    }
    

    このコードは、 Student ASP.NET MVC モデル バインダーによって作成されたエンティティを Students エンティティ セットに追加し、変更をデータベースに保存します。 (モデル バインダー は、フォームによって送信されたデータを簡単に操作できるようにする ASP.NET MVC 機能を指します。モデル バインダーは、ポストされたフォーム値を CLR 型に変換し、パラメーターのアクション メソッドに渡します。この場合、モデル バインダーは、コレクションのプロパティ値を Student 使用してエンティティを Form インスタンス化します)。

    属性はValidateAntiForgeryToken、クロスサイト リクエスト フォージェリ攻撃を防ぐのに役立ちます。

> [!WARNING]
    > Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
    > 
    > [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
    > 
    > Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
    > 
    > ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)  
    > 
    > The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
    > 
    > It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
    > 
    > Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.

    Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).

    The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:

    [!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]

    *Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.

    No changes are required in *Create.cshtml*.
  1. [ 学生 ] タブを選択し、[ 新規作成] をクリックして、ページを実行します。

    Student_Create_page

    一部のデータ検証は既定で機能します。 名前と無効な日付を入力し、[ 作成 ] をクリックしてエラー メッセージを表示します。

    Students_Create_page_error_message

    次の強調表示されたコードは、モデルの検証チェックを示しています。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(student);
    }
    

    日付を 2005 年 9 月 1 日などの有効な値に変更し、[ 作成 ] をクリックすると、新しい学生が [インデックス ] ページに表示されます。

    Students_Index_page_with_new_student

POST ページの編集

Controllers\StudentController.cs では、 HttpGetEdit メソッド (属性のないHttpPostメソッド) は、 メソッドをFind使用して選択したStudentエンティティをDetails取得します。これは、 メソッドで確認したとおりです。 このメソッドを変更する必要はありません。

ただし、action メソッドを HttpPostEdit 次のコードに置き換えて、 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);
}

このコードは、 メソッドで見たものと HttpPostCreate 似ています。 ただし、モデル バインダーによって作成されたエンティティをエンティティ セットに追加する代わりに、このコードはエンティティに変更されたことを示すフラグを設定します。 SaveChanges メソッドが呼び出されると、Modified フラグによって Entity Framework によって SQL ステートメントが作成され、データベース行が更新されます。 ユーザーが変更しなかった列を含め、データベース行のすべての列が更新され、コンカレンシーの競合は無視されます。 (このシリーズの後のチュートリアルでは、コンカレンシーを処理する方法について説明します)。

エンティティの状態と Attach メソッドと SaveChanges メソッド

データベース コンテキストは、メモリ内のエンティティがデータベースの対応する行と同期しているかどうかを追跡しており、この情報により、SaveChanges メソッドを呼び出したときの処理が決まります。 たとえば、新しいエンティティを Add メソッドに渡すと、そのエンティティの状態は に Added設定されます。 次に、 SaveChanges メソッドを呼び出すと、データベース コンテキストによって SQL INSERT コマンドが発行されます。

エンティティは、次のいずれかの状態になります。

  • Added. エンティティはまだデータベースに存在しません。 メソッドは SaveChanges ステートメントを発行する INSERT 必要があります。
  • Unchanged. SaveChanges メソッドはこのエンティティに対し何も行う必要はありません。 データベースからエンティティを読み取ると、エンティティはこの状態で開始します。
  • Modified. エンティティのプロパティ値の一部またはすべてが変更されています。 メソッドは SaveChanges ステートメントを発行する UPDATE 必要があります。
  • Deleted. エンティティには削除のマークが付けられています。 メソッドは SaveChanges ステートメントを発行する DELETE 必要があります。
  • Detached. エンティティはデータベース コンテキストによって追跡されていません。

デスクトップ アプリケーションにおいて、通常、状態の変更は自動的に設定されます。 デスクトップの種類のアプリケーションでは、エンティティを読み取り、そのプロパティ値の一部を変更します。 そのエンティティの状態は自動的に Modified に変更されます。 次に、 を呼び出 SaveChangesすと、変更した実際のプロパティのみを更新する SQL UPDATE ステートメントが Entity Framework によって生成されます。

Web アプリの切断された性質では、この連続したシーケンスは許可されません。 エンティティを読み取る DbContext は、ページのレンダリング後に破棄されます。 HttpPostEditアクション メソッドが呼び出されると、新しい要求が行われ、DbContext の新しいインスタンスが作成されるため、エンティティの状態を手動で に設定するModified.必要があります。 を呼び出SaveChangesすと、コンテキストで変更したプロパティを知る方法がないため、Entity Framework によってデータベース行のすべての列が更新されます。

ユーザーが実際に変更したフィールドのみを SQL Update ステートメントで更新する場合は、元の値 (非表示フィールドなど) を何らかの方法で保存して、メソッドの呼び出し時 HttpPostEdit に使用できるようにします。 次に、元の値を使用してエンティティを作成 Student し、その元のバージョンのエンティティで メソッドを呼び出 Attach し、エンティティの値を新しい値に更新してから、 を呼び出 SaveChanges. します。詳細については、MSDN Data Developer Center の 「Entity states」と「SaveChanges and Local Data 」を参照してください。

Views\Student\Edit.cshtml のコードは、Create.cshtml で確認したコードと似ていますが、変更は必要ありません。

[ 学生 ] タブを選択し、[ 編集 ] ハイパーリンクをクリックして、ページを実行します。

Student_Edit_page

データをいくつか変更し、 [Save] をクリックします。 [インデックス] ページに変更されたデータが表示されます。

Students_Index_page_after_edit

削除ページの更新

Controllers\StudentController.cs のメソッドのテンプレート コードHttpGetDeleteでは、 メソッドと Edit メソッドで確認したように、 メソッドを使用Findして選択したStudentエンティティをDetails取得します。 ただし、SaveChanges の呼び出しが失敗したときのカスタム エラー メッセージを実装するには、何らかの機能とその対応するビューをこのメソッドに追加します。

更新および作成操作で見たように、削除操作にも 2 つのアクション メソッドが必要です。 GET 要求に応答して呼び出されるメソッドには、削除操作を承認または取り消す機会をユーザーに提供するビューが表示されます。 ユーザーが操作を承認すると、POST 要求が作成されます。 その場合、 HttpPostDelete メソッドが呼び出され、そのメソッドは実際に削除操作を実行します。

データベースの更新時に発生するHttpPostDelete可能性のあるエラーを処理するブロックを メソッドに追加try-catchします。 エラーが発生した場合、メソッドは HttpPostDelete メソッドを HttpGetDelete 呼び出し、エラーが発生したことを示すパラメーターを渡します。 次に、 メソッドは HttpGet Delete 確認ページとエラー メッセージを再表示し、ユーザーにキャンセルまたは再試行を行う機会を提供します。

  1. action メソッドを HttpGetDelete 、エラー報告を管理する次のコードに置き換えます。

    public ActionResult Delete(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)
        {
            return HttpNotFound();
        }
        return View(student);
    }
    

    このコードは、変更の保存に失敗した後に呼び出されたかどうかを示す 、オプション の Boolean パラメーターを受け入れます。 このパラメーターは、 false メソッドが以前の HttpGetDelete エラーなしで呼び出された場合です。 データベース更新エラーに応答して メソッドによって HttpPostDelete 呼び出されると、 パラメーターが になり true 、エラー メッセージがビューに渡されます。

  2. HttpPostDeleteアクション メソッド (という名前DeleteConfirmed) を、実際の削除操作を実行し、データベース更新エラーをキャッチする次のコードに置き換えます。

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

    このコードは、選択したエンティティを取得し、 Remove メソッドを呼び出してエンティティの状態を に Deleted設定します。 SaveChanges が呼び出された場合、SQL の DELETE コマンドが生成されます。 また、アクション メソッドの名前を DeleteConfirmed から Delete に変更しています。 メソッドに一意のシグネチャを HttpPostDelete 与える メソッド DeleteConfirmed という名前の HttpPost スキャフォールディング されたコード。 ( CLR では、異なるメソッド パラメーターを持つオーバーロードされたメソッドが必要です。シグネチャが一意になったので、MVC 規則に従い、 メソッドと HttpGet delete メソッドに同じ名前をHttpPost使用できます。

    大量のアプリケーションでのパフォーマンスの向上が優先される場合は、不要な SQL クエリを回避し、メソッドと Remove メソッドを呼び出すFindコード行を黄色の強調表示に示すように次のコードに置き換えることで、行を取得できます。

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

    このコードでは、 Student 主キー値のみを使用してエンティティをインスタンス化し、エンティティの状態を に Deleted設定します。 エンティティを削除するために Entity Framework に必要なものは主キーの値だけです

    前に示したように、 HttpGetDelete メソッドはデータを削除しません。 GET 要求に応答して削除操作を実行すると (つまり、編集操作、作成操作、またはデータを変更するその他の操作を実行する)、セキュリティ 上のリスクが生じます。 詳細については、「 ASP.NET MVC ヒント #46 - Stephen Walther のブログにセキュリティ ホールが作成されるため、リンクの削除を使用しないでください 」を参照してください。

  3. Views\Student\Delete.cshtml で、次の例に示すように、見出しと見出しのh3h2にエラー メッセージを追加します。

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

    [ 学生 ] タブを選択し、[ 削除 ] ハイパーリンクをクリックして、ページを実行します。

    Student_Delete_page

  4. [Delete] をクリックします。 削除された学生を含まない [Index] ページが表示されます (このシリーズの後半の「 コンカレンシー の処理」チュートリアルで、動作中のエラー処理コードの例が表示されます)。

データベース接続を開いたままにしないようにする

データベース接続が適切に閉じられ、保持されているリソースが解放されていることを確認するには、コンテキスト インスタンスが破棄されていることを確認する必要があります。 そのため、スキャフォールディングされたコードは、次の例に示すように、StudentController.cs のクラスのStudentController末尾に Dispose メソッドを提供します。

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

基底 Controller クラスは既に インターフェイスを IDisposable 実装しているため、このコードは単純に メソッドにオーバーライドを追加して Dispose(bool) 、コンテキスト インスタンスを明示的に破棄します。

まとめ

これで、エンティティに対 Student して単純な CRUD 操作を実行するページの完全なセットが作成されました。 MVC ヘルパーを使用して、データ フィールドの UI 要素を生成しました。 MVC ヘルパーの詳細については、「 HTML ヘルパーを使用したフォームのレンダリング 」を参照してください (このページは MVC 3 用ですが、MVC 4 にはまだ関連しています)。

次のチュートリアルでは、並べ替えとページングを追加することで、インデックス ページの機能を拡張します。

他の Entity Framework リソースへのリンクは、 ASP.NET データ アクセス コンテンツ マップにあります。