by WebSurfer
2016年9月11日 14:08
先に、Entity Framework Code First の機能を利用して SQL Server データベースに親子関係のあるテーブルを生成し、ASP.NET MVC4 で登録・編集・削除を行うという以下の 3 つの記事を書きました。
-
MVC4 EF Code First(Entity Framework Code First の機能を利用して SQL Server 2008 Express に MVC4 アプリケーション用のテーブルを生成)
-
親子関係のあるデータ登録(Create アクションメソッド / ビューを追加して、上記 1 で作成したテーブルにデータを登録)
-
親子関係のあるデータの編集・削除(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 がスローされる。