WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

MVC アプリでのデータの編集・更新

by WebSurfer 2016年9月18日 13:48

ASP.NET MVC アプリケーションで Entity Framework を利用してデータベースの編集・更新を行う方法について調べる機会があったのですが、その過程でいろいろ新発見があったので備忘録として残しておきます。

(1) EntityState.Modified の設定

データベースを更新 (UPDATE) するには、SaveChanges メソッドをコンテキストに適用する前に、当該エンティティの State を EntityState.Modified(以下、Modified と書きます)に設定する必要があります。

その方法には、自分が知る限りですが、以下の 2 つがあります:

(1-1) コードで明示的に設定

編集したオブジェクトをコンテキストにアタッチし、そのエンティティの State を Modified に設定します。

具体的な方法は、Microsoft の記事「Working with entity states」の Attaching an existing but modified entity to the context のセクションの説明とサンプルコードが参考になると思いますので、そちらを見てください。

なお、上に紹介した記事のコードは DbContext クラスがベースのコンテキストの場合ですので注意してください。(EF6 Code First の場合は DbContext クラスが使われます)

VS2010 の EF4 を使って DB First で作った EDM などには、DbContext クラスではなくて ObjectContext クラスが使われますが、それには Entry メソッドは定義されておらず、上に紹介した記事のコードのようにはできないので注意してください。具体的には以下のようにします。

[HttpPost]
public ActionResult Edit(Address address)
{
  if (ModelState.IsValid)
  {
    db.Address.Attach(address);
    db.ObjectStateManager.
      ChangeObjectState(address, EntityState.Modified);
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(address);
}

上のコードは、Microsoft が提供するサンプルデータベース AdventureWorksLT の Address テーブルから、VS2010 + EF4 を使って DB First で EDM を作成し、それをベースにスキャフォールディング機能を使って自動生成させたものです。

(1-2) エンティティのプロパティを書換

コンテキストから更新するエンティティを取得し、そのプロパティを使って内容を書き換えると、前の値から変更された場合は自動的にそのエンティティに Modified マークが付けられます。前の値と同じ値を設定した場合は Unchanged のままとなります。

言葉だけでは分かりにくいと思いますので、以下にコードを書いて説明します。

先の記事「EF でレコードの削除」で紹介した通りに Code First でデータベースを生成したとします。

Code First で生成した Blogs テーブルと Posts テーブルに、以下のコードでデータを INSERT します。

// DB に以下のデータを INSERT
Blog b = new Blog { Name = "プログラミング" };
b.Posts = new List<Post>();
b.Posts.Add(new Post { Title = "ASP", Content = "作り方" });
b.Posts.Add(new Post { Title = "WCF", Content = "書き方" });
b.Posts.Add(new Post { Title = "WPF", Content = "内容" });
db.Blogs.Add(b);
db.SaveChanges();

それを以下のようなコードで各エンティティのプロパティを使用して書き換えるとします。

// DB からエンティティを取得し、一部のプロパティを書換
BloggingContext db = new BloggingContext();
Blog b = db.Blogs.Single(i => i.BlogId == 1);
b.Name = "プログラミング";     // 変更なし
b.Posts[0].Title = "MVC";      // ASP ⇒ MVC
b.Posts[0].Content = "訂正";   // 作り方 ⇒ 訂正
b.Posts[1].Title = "NET";      // WCF ⇒ NET
b.Posts[1].Content = "書き方"; // 変更なし
b.Posts[2].Title = "WPF";      // 変更なし
b.Posts[2].Content = "内容";   // 変更なし

// 各エンティティの State を調べると:
EntityState state = db.Entry(b).State;  // Unchanged
foreach (Post p in b.Posts)
{
    state = db.Entry(p).State;
    // 順に、Modified, Modified, Unchanged
}

各エンティティの State を調べると、全てのプロパティに現在の値と同じ値を設定した場合は(即ち変更しない場合は)そのエンティティの State は Unchanged のままになり、一つでもプロパティが前の値から変更された場合はエンティティの State が自動的に Modified に変更されます。

注:Visual Studio 2010 に標準で備わっている EF4 を使って DB First で EDM を生成した場合はプロパティに現在の値と同じ値を設定しても Modified に変わるので注意してください。上の話は NuGet で EF6 を適用し、Code First でデータベースを生成した場合の話です。(EF4 / EF6 のバージョンの差によるものか、ObjectContext / DbContext のコンテキストのベースが違うためかは分かりません)


(2) MVC アプリで Modified マークをつける方法

エンティティに Modified マークをつけてから、そのエンティティをトラックしているコンテキストに SaveChanges メソッドを適用すればデータベースは更新 (UPDATE) されます。

ASP.NET MVC アプリでエンティティに Modified マークをつけるには、編集画面から送信されてきたユーザー入力情報を利用することになりますが、基本的には以下のような方法があると思います。

(2-1) UpdateModel メソッドを使う

Controller.UpdateModel メソッドは、フォーム、クエリ文字列、ルート、クッキーなどに含まれるクライアントから送信されてきたデータから、このメソッドの引数に設定されたエンティティのプロパティ名と一致するものを探して、それでエンティティの内容を書き換えます。

書き換えられると、上の (1-2) で書いたように、State が自動的に Unchanged から Modified に変わりますので、SaveChanges メソッドでデーターベースが更新 (UPDATE) されます。

UpdateModel メソッドを使う場合の問題は、フォーム、クエリ文字列、ルート、クッキーなどを使って送信されてきたデータのどれを使ってエンティティを書き換えているのかよく見えないところにあると思います。UpdateModel には多数のオーバーロードがあり、送信されたパラメータを指定 / 除外するためのオプションがあって、それらを使えばある程度コントロールできるとは思いますが・・・

そのあたりを配慮するとしても、先の記事「親子関係のあるデータの編集・削除」で書いたようなケースが問題です。

その記事に書いてありますが、UpdateModel メソッドを使うと、SaveChanges で子テーブルの関連するレコードの外部キーフィールドを NULL に書き換え、ポストされたデータでレコードを新たに作り INSERT するという動きになります。外部キーが NULL 不可に設定されている場合は特に問題で、当然制約違反でエラーになってしまいます。

これには、自分が考えた限りですが、対応不可でした。(何故そういう結果になるのか、メカニズムが解明できていません)

そもそも、普通はモデルバインディング+データアノテーション検証の機能を利用している場合がほとんどでしょうから、そういうケースでは UpdateModel メソッドは忘れてもよさそうです。

モデルバインディング+データアノテーション検証の機能を利用すると、アクションメソッドのパラメータ(引数)が指すオブジェクトにクライアントから送信されてきたデータがバインドされると同時に、サーバー側で検証が行われます。

コントローラーで ModelState.IsValid が ture であればアクションメソッドの引数が指すオブジェクトにバインドされたデータは検証結果 OK ということになります。

従って、モデルバインディング+データアノテーション検証の機能を利用しているのであれば、アクションメソッドの引数を使った方が簡単&確実だと思います。その方法は下の (2-2), (2-3) を見てください。

(2-2) バインディング結果をアタッチして Modified マーク

モデルバインディング+データアノテーション検証の機能を利用している場合、アクションメソッドの引数が指すオブジェクトに更新後のデータはバインドされており、コントローラーで ModelState.IsValid が ture であればデータは検証結果 OK ということになります。

従って、アクションメソッドの引数が指すオブジェクトを、上の (1-1) で述べた方法���コンテキストにアタッチし、そのエンティティの State を Modified に設定してやることで目的が果たせます。

UpdateModel メソッドでは (2-1) に書いた問題があった親子関係のあるデータ(子はコレクション)の場合も、親子を別々にコンテキストにアタッチし、State を Modified に設定してやれば問題は解決できます。(具体例は、先の記事「親子関係のあるデータの編集・削除」の「Edit アクションメソッド」のコードを見てください)

(2-3) バインドされたデータでエンティティを書き換え

上の (2-2) の方法では無条件に State が Midified に設定されますので、ユーザーがデータを変更していなくても SaveChanges でデータベースに UPDATE がかかってしまいます。

先の記事「親子関係のあるデータの編集・削除」ではそのようにコーディングしましたが、子のデータのほんの一部のみ更新するケースも多々あるでしょうから考え直した方がよさそうです。

また、本来更新は不要でユーザーが送信する必要のないデータも送信する必要があるという面倒なこともあります。特に外部キーのデータは送信しないと State を Midified に設定する際参照整合性制約違反でエラーとなります。

上の (1-2) で書きましたが、コンテキストから更新するエンティティを取得し、そのプロパティを使って内容を書き換えると、前の値から変更された場合のみ Modified マークが付けられ、前の値と同じ値を設定した場合は Unchanged のままとなります。

アクションメソッドの引数が指すオブジェクトにモデルバインドされたデータがあるのですから、自力でコードを書いて更新する項目のみそれらのデータをエンティティに反映させた方がよさそうです。すべてを自分のコントロール下で設定できるので安心・安全ということもあるかもしれません。

上記のことを考慮して、先の記事「親子関係のあるデータの編集・削除」を書き直すと、以下のようになるでしょうか。

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

namespace Mvc4App3.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(Parent postedParent)
    {
      if (ModelState.IsValid)
      {
        // 編集画面から送信された親の Id で、コンテキストから
        // 該当する親エンティティを取得
        Parent parent = db.Parents.Find(postedParent.Id);

        if (parent == null)
        {
          // 見つからなかったときの処理
        }

        // 編集画面から送信されたデータで、上で取得した親エン
        // ティティの中身を書き換え
        parent.Name = postedParent.Name;

        foreach (Child postedChild in postedParent.Children)
        {
          // 編集画面から送信された子の Id で、コンテキスト
          // から該当する子エンティティを取得
          Child child = db.Children.Find(postedChild.Id);
                    
          if (child == null)
          {
            // 見つからなかったときの処理
          }
                    
          // 編集画面から送信されたデータで、上で取得した
          // 子エンティティの中身を書き換え
          child.Name = postedChild.Name;
        }

        db.SaveChanges();

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

    // ・・・中略・・・

  }
}

上のコードのコメントの「見つからなかったときの処理」ですが、編集画面を表示する際に取得した Id を隠しフィールドで保持しており、更新するときそれをそのまま POST しているので、普通なら見つからないということはあり得ません。見つからないとすれば、編集中に誰かが DB の当該レコードを削除してしまったとか、不正なデータが送信されたなどの異常事態と考えた方がよいかもしれません。

Tags: ,

MVC

Code First で外部キープロパティの定義

by WebSurfer 2016年9月11日 14:08

先に、Entity Framework Code First の機能を利用して SQL Server データベースに親子関係のあるテーブルを生成し、ASP.NET MVC4 で登録・編集・削除を行うという以下の 3 つの記事を書きました。

  1. MVC4 EF Code First(Entity Framework Code First の機能を利用して SQL Server 2008 Express に MVC4 アプリケーション用のテーブルを生成)
  2. 親子関係のあるデータ登録(Create アクションメソッド / ビューを追加して、上記 1 で作成したテーブルにデータを登録)
  3. 親子関係のあるデータの編集・削除(Edit および Delete アクションメソッド / ビューを追加して、上記 2 で登録したデータを編集・削除)

上の記事では、テーブル生成のベースとなる Child クラスに Parent クラスを参照するナビゲーションプロパティと外部キープロパティは定義していませんでした。(具体的には上記 1 の記事の Model のコードを見てください)

それに、Microsoft の文書「Code First 規約」の「リレーションシップ規約」セクションに書いてある "型には、ナビゲーションプロパティに加え、依存オブジェクトを表す外部キーのプロパティを追加することをお勧めします" に従って、ナビゲーションプロパティと外部キープロパティを定義するとどのような影響があるかを書きます。

追加後のコードは以下の通りです。上で言う「依存オブジェクトを表す」型は Parent クラスですので、Child クラスに Parent クラスへのナビゲーションプロパティと外部キープロパティを追加します。外部キープロパティは int 型とし、Children テーブルに生成される外部キーフィールドを NULL 不可に(連鎖削除を設定)します。

public class Child
{
    public int Id { get; set; }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "Child Name")]
    public string Name { get; set; }

    // 外部キープロパティを追加
    // int 型なので外部キーフィールドは NULL 不可になる
    public int ParentId { get; set; }

    // ナビゲーションプロパティを追加
    public virtual Parent Parent { get; set; } 
}

その後、上記 1 の記事「MVC4 EF Code First」に書いた手順に従って、Controller と View を作って Controller のアクションメソッドを呼び出すと、以下とおり Parents テーブルと Children テーブルが自動的に生成されます。

生成されたテーブル

Child クラスへの int 型外部キープロパティ ParentId の追加により、Children テーブルの外部キーフィールドがプロパティ名と同名の ParentId となり NULL 不可に設定されたことが上記 1 の記事で作成したデータベースと異なる点です。

ASP.NET MVC4 アプリからこれらのテーブルへのデータの登録は、上記 2 の記事「親子関係のあるデータ登録」の手順どおりで可能です。

編集・削除は上記 3 の記事「親子関係のあるデータの編集・削除」とは若干異なってきます。注意すべき点を以下に箇条書きにします。

編集 (Edit)

  • Child クラスに ParentId プロパティが定義されているので ParentId の値もポストする必要がある。Id と同様に View に隠しフィールドを追加して対応する。そうしないと、モデルバインディングの際 ParentId プロパティにはゼロがバインドされ、db.Entry(postedParent).State を Modified に設定する時に参照整合性制約違反でエラーになる。
  • UpdateModel(db.Parents.Find(id)) の後 db.SaveChanges するのはエラーになる。Children テーブルの関連するレコードの外部キーフィールド ParentId を NULL に書き換え、ポストされたデータでレコードを新たに作り INSERT するという動きになるが、外部キーフィールド ParentId は NULL 不可なのでエラーになる。(先の記事の例のように外部キーフィールドが NULL 可であればエラーは出ない。外部キーフィールドが NULL の余計なレコードは残るが DB の整合性は保たれる)
  • db.Entry(親).State を Modified にしただけでは、先の記事の例と同様、親しか更新されない。コードを書いて子の State も Modified に設定する必要がある。(実は、連鎖削除だけでなく連鎖更新もされるのではと期待したがダメでした)
  • 親も子も無条件で State を Modified に設定すると、変更する必要のないレコードまで UPDATE されてしまう。先の記事の例ではそうコーディングしたが、考え直した方がよさそう。具体的な案としては、db.Parents.Find(id) で Parent オブジェクトを取得し、そのプロパティをポストされた値(アクションメソッドの引数 postedParent から取得)で一つ一つ書き換えるのがよさそう。そうすると、前の値と変更になった場合のみ自動的に Modified マークが付けられ、db.SaveChanges で更新される。(前の値と変わらなければ Unchanged マークのままなので更新されない) この方法を取れば、View の隠しフィールドで Id や ParentId をポストする必要もなくなる。

削除 (Delete)

  • Microsoft の文書「Code First の規約」によると、"依存エンティティの外部キーで null 値が許容されない場合、Code First はリレーションシップに連鎖削除を設定します" とのこと。外部キーフィールド ParentId が NULL 不可なので、フレームワークはリレーションシップに連鎖削除を設定しているはず。実際に削除を試してみると、親だけ Remove すれば連鎖的に子も削除されることを確認できた。(上記 3 の記事のように、子を Remove するのは不要)
  • 同じ Microsoft の文書には "依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます" と書いてあるが、上記 3 の記事(外部キーは NULL 可)で試した限りではそのようにはならない。子を持つ親を削除しようとすると FK 制約に引っかかって SqlException がスローされる。

Tags: ,

MVC

IE11 で[戻る]ボタンが使えない

by WebSurfer 2016年8月24日 10:37

ユーザーが Power Users グループに属していると IE11 の[戻る]ボタンが使えない(グレーアウトされる)ことがあるという話を書きます。

Power Users グループ

元は MSDN Forum の「IE11で戻るボタンが使えない」という表題のスレッドでの話です。

そのスレッドを読めば話はすぐわかるのですが、備忘録として自分のブログの記事にも書いておくことにしました。

簡単に話の内容を書くと・・・

IE11 にアップグレードしたら一部のドメインユーザーで IE11 の[戻る]ボタンが使えなくなる(グレーアウトされる)という問題が発生。

ググって調べてみると、ユーザーを Power Users グループから外したら問題が解決したという記事を発見。(ただし原因は不明)

MSDN Forum の質問者さんの方でも、ユーザーを Power Users グループから外すことで問題が解消した。

・・・ということです。

ネットの情報だけで自分で検証したわけではないし、そもそも何故 Power Users グループが影響するのか不明というのがアレですが、IE11 の[戻る]ボタンが使えないという問題に遭遇したら Power Users グループに属していないか調べてみるのがよさそうです。

Tags: ,

その他

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar