WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

親子関係のあるデータの編集・削除

by WebSurfer 22. December 2014 12:08

先に、(1) Entity Framework Code First の機能を利用して MVC4 アプリケーション用 SQL Server DB のテーブル作成、(2) Create アクションメソッドとビューを追加して作成したテーブルに親子関係のあるデータを登録する方法・・・という記事を書きました。

今回はそれに続いて Edit, Delete アクションメソッドとビューを追加し、先に追加した Create アクションメソッドで登録したデータの編集・削除を行う方法を書きます。(下の画像は Delete 操作のときのものです)

データの削除画面

先の記事 MVC4 EF Code First で書きましたように、Code First の機能を用いて生成した SQL Server DB の Parents, Children テーブルの間に外部キー制約が設定されています(Parents の Id ← Children の Parent_Id)。

なので、階層更新が必要になります。つまり、登録する場合は Parents テーブルに親レコードを INSERT した後 Children テーブルに子レコードを INSERT する、削除する場合は先に Children テーブルの関連する子レコードを全部 DELETE してから Parents テーブルの親レコードを DELETE するという操作が必要になります。

そのために Entity Framework 上で必要な操作としては、対象となるレコードの エンティティオブジェクトの状態 を、登録なら Added、編集なら Modified、削除なら Deleted としてマークし、DbContext.SaveChanges メソッド を適用すればよさそうです。

階層更新(上の例で言うと、INSERT, DELETE するときの順番)や、INSERT 時に Parents テーブルの親レコードの Id(IDENTITY 列)から値を取得して Children テーブルの子レコードの Parent_Id に設定するという操作は Entity Framework が面倒を見てくれるようです。(それを書いた公式文書が見つけられず、検証した結果だけ見てそう言っているので、100% の自信はないですけど・・・)

従って、プログラマが行うべきことで重要なのは、対象エンティティオブジェクトの状態を正しく Added, Modified, Deleted に設定してやると言うことになります。

具体的な方法は、文章で書くよりはコードを示した方がわかりやすいと思いますので、Edit, Delete 操作用のアクションメソッドと View のコードを下にアップしました。

自分が犯した失敗例や、必要な(と自分が思った)コメントも書いておきました。参考になれば幸いです。

Edit アクションメソッド

注意事項はコード内のコメントに書きましたので、それを参照してください。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App2.Models;

namespace Mvc4App2.Controllers
{
  public class ParentChildController : Controller
  {
    private ParentChildContext db = new ParentChildContext();
        
    //・・・中略・・・

    //
    // GET: /ParentChild/Edit/5
    public ActionResult Edit(int id)
    {
      Parent parent = db.Parents.Find(id);
      if (parent == null)
      {
        return HttpNotFound();
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(int id, Parent postedParent)
    {
      if (ModelState.IsValid)
      {
        // 以下のコードはダメ。
        // ポストされた Name の子レコードが新手に作られて
        // INSERT され、その Parent_Id が親の Id に設定さ
        // れる。既存の子レコードの Parent_Id は NULL に
        // 書き換えられる。

        //Parent parent = db.Parents.Find(id);
        //UpdateModel<Parent>(parent);
        //db.SaveChanges();

  
        // 以下のコードもダメ。
        // 親レコードしか書き換えられない。

        //db.Entry(postedParent).State = EntityState.Modified;
        //db.SaveChanges();


        // 以下のように子の方のエンティティ状態も 'Modified'
        // に設定すると親も子も期待通り更新される。

        for (int i = 0; i < postedParent.Children.Count; i++)
        {                    
          db.Entry(postedParent.Children[i]).State = 
                                       EntityState.Modified;
        }
        db.Entry(postedParent).State = EntityState.Modified;
        db.SaveChanges();

        return RedirectToAction("Index");
      }
      return View(postedParent);
    }

    //・・・中略・・・

  }
}

View (Edit.cshtml)

Parents, Children とも Id が隠しフィールド(@Html.HiddenFor)に設定されている点に注意してください。

コードの最後の方の @Scripts.Render("~/bundles/jqueryval") は入力検証用の jQuery ライブラリを登録するためのものです。これがないとクライアント側での検証はかかりませんので注意してください。

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Edit";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
  @Html.AntiForgeryToken()
  @Html.ValidationSummary(true)

  <fieldset>
    <legend>Parent</legend>

    @Html.HiddenFor(model => model.Id)

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

    <hr />

    @for (int i = 0; i < Model.Children.Count; i++)
    {       
      @Html.HiddenFor(model => model.Children[i].Id)
            
      <div class="editor-label">
        @Html.LabelFor(model => 
                        model.Children[i].Name)
      </div>
      <div class="editor-field">
        @Html.EditorFor(model => 
                        model.Children[i].Name)
        @Html.ValidationMessageFor(model => 
                        model.Children[i].Name)
      </div>
                
      <hr />
    }

    <p>
      <input type="submit" value="Save" />
    </p>
  </fieldset>
}

<div>
  @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

Delete アクションメソッド

注意事項はコード内のコメントに書きましたので、それを参照してください。


    //・・・前略・・・

    //
    // GET: /ParentChild/Delete/5
    public ActionResult Delete(int id)
    {
      Parent parent = db.Parents.Find(id);
      if (parent == null)
      {
        return HttpNotFound();
      }
      return View(parent);
    }

    //
    // POST: /ParentChild/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
      // 以下のコードはダメ。
      // 子のデータががある場合、FK 制約に引っかかって
      // SqlException がスローされる。

      //Parent parent = db.Parents.Find(id);
      //db.Parents.Remove(parent);

            
      // 以下のコードも��メ。
      // 親と1 つ目の子レコードは削除されるが 2 つ目が残ってし
      // まう。(ただし残った子レコードの Parent_Id は NULL に
      // 書き換えられるので FK 制約には引っかからない。何故?)
      // Remove すると、その度 parent.Children.Count が一つ減っ
      // てしまう。そのため 1 つ目の子レコードを Remove した後
      // ループを抜けてしまい、2 つ目が Remove できないので、
      // db.SaveChanges() しても 2 つ目が残ってしまう。

      //Parent parent = db.Parents.Find(id);
      //for (int i = 0; i < parent.Children.Count; i++)
      //{
      //    db.Children.Remove(parent.Children[i]);
      //}
      //db.Parents.Remove(parent);            
      //db.SaveChanges();


      // 以下のように一旦 Child のコレクションを保持しておき、
      // それを使って Remove すれば OK。

      Parent parent = db.Parents.Find(id);

      List<Child> children = new List<Child>();
      foreach (Child child in parent.Children)
      {
        children.Add(child);
      }

      foreach (Child child in children)
      {
        db.Children.Remove(child);
      }
      db.Parents.Remove(parent);            
      db.SaveChanges();

      return RedirectToAction("Index");
    }

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

View (Delete.cshtml)

form 要素には action="/ParentChild/Delete/21" というように設定されます(21 は親レコードの Id)。なので、[Delete]ボタンをクリックして POST すると、アクションメソッド DeleteConfirmed(int id) の引数には親レコードの Id が渡されます。

@model Mvc4App2.Models.Parent

@{
  ViewBag.Title = "Delete";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
  <legend>Parent</legend>
  <div class="display-label">
     @Html.DisplayNameFor(model => model.Id)
  </div>
  <div class="display-field">
    @Html.DisplayFor(model => model.Id)
  </div>
  <div class="display-label">
     @Html.DisplayNameFor(model => model.Name)
  </div>
  <div class="display-field">
    @Html.DisplayFor(model => model.Name)
  </div>

  <hr />

  @for (int i = 0; i < Model.Children.Count; i++)
  {       
    <div class="display-label">
      @Html.DisplayNameFor(model => 
                      model.Children[i].Id)           
    </div>
    <div class="display-field">
      @Html.DisplayFor(model => 
                      model.Children[i].Id)
    </div>
        
    <div class="display-label">
      @Html.DisplayNameFor(model => 
                      model.Children[i].Name)           
    </div>
    <div class="display-field">
      @Html.DisplayFor(model => 
                      model.Children[i].Name)
    </div>
    <hr />
  }

</fieldset>
@using (Html.BeginForm()) {
  @Html.AntiForgeryToken()
  <p>
    <input type="submit" value="Delete" /> |
    @Html.ActionLink("Back to List", "Index")
  </p>
}

次の課題は、(1) 既存の親に属する子の全部または一部を DELETE、(2) 既存の親に子を追加・・・をどう実装するかですね。やる気が湧いてきたら書いてみます。(笑)

さらなる課題は、同時実行制御や、エラーの際のロールバックをどう実装するかでしょうか。そこのところは勉強不足でまだ見当さえついてません。先はずいぶん長そうです。(汗)

2016/9/12 追記:
Microsoft の文書「Code First の規約」に従って、Child クラスにナビゲーションプロパティと外部キープロパティを定義するとどのような影響があるかを別の記事「Code First で外部キープロパティの定義」に書きました。int 型の外部キープロパティを Child クラスに追加にしたのですが、それによる大きな影響は以下の 2 点です。他にも影響はありますが、詳しくは上にリンクを張った別記事を見てください。
  • 編集・更新: Child クラスに外部キープロパティを追加したので、編集結果を送信して更新をかける際、外部キープロパティの値も送信する必要がある。そうしないと参照整合性制約違反でエラー。
  • 削除: 外部キープロパティを int 型にしたので、外部キーフィールドが NULL 不可になり、連鎖削除が可能になる。

Tags: ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar