MicrosoftのEntity Framework が基本的に単純な単一の外部キーで失敗するのはなぜですか?
フリークエント フライヤー プログラムについては誰もが聞いたことがあるでしょう。Edison に触発された私のよくある失敗プログラムに参加して、EF を使用しない方法を学びましょう。私が試したコードと、以下の対応するエラーを確認してください。
Morteza Manaviは、この1 対 1 の主キーの関連付けまたは共有主キーの問題について説明しています。Telerik のOpenAccess ORM は、これを「垂直継承」と呼んでいます。EF には、Ruby On Rails のActiveRecord belongs_toのような一貫した用語やプロパティがありません。
この SQL Server ダイアグラムに示されている 2 つのテーブル、外部キー、および一意の制約を構築する T-SQL スクリプトは、すぐにプロトタイプを作成して再生したい場合に以下に含まれています...
教育的ユースケース
このシナリオには多くはありません。1 対多または多対多の回答を探している場合は、申し訳ありませんが、間違った場所にいます。多重度は 1 人から 0..1 メールです。
ユーザーが Razor フォームで電子メールを空白のままにした場合、Person と ContactInfoes の関係は 1:0 (つまり、1 対 1 [12n1]) で、ContactInfoes レコードはありません。
それ以外の場合、ContactInfoes は People と 1:1 (つまり、1 対 1 [121]) の関係になります。または、People レコードは ContactInfoes レコードと 1 対 1 の関係にあります。
私は何年もの間、鳴き声 [SQL] を作成してきましたが、上の EDMX スクリーン ショットでは、バックグラウンドで何が起こっているのかを完全に把握することはできません。1 対 1 の EF とCode Firstererを使用するために、以下の DbContext Code First モデルにリストされているように、他のエンティティを参照するエンティティごとに仮想ICollectionを作成することにより、複雑さが増します。
public class MVC3EF4Context : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Person> People { get; set; }
public System.Data.Entity.DbSet<ContactInfo> ContactInfoes { get; set; }
}
public class Person
{
[Key]
public int PersonId { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<ContactInfoes> ContactInfo { get; set; }
}
public class ContactInfo
{
[Key]
[ForeignKey("PersonId")]
public int PersonId { get; set; }
[Required]
public string Email { get; set; }
public virtual ICollection<People> Person { get; set; }
}
この追加の複雑さの利点は、厳密に型指定されたコントローラーの IntelliSense と、EDMX ダイアグラム、T4 .tt ファイル、または Code First クラスから取得できる People モデルからコード ジェネレーターが作成するビューです。
Noob の EF を直感的に使用する@Html.EditorFor(model => model.**ContactInfoes.Email**)
には、Create.cshtml と Edit.cshtml に自動的に生成された個人用の Razor フォームを追加するだけです。
失敗 失敗 失敗
EF はエンティティの完全なグラフを作成するため、電子メールがある場合、ContactInfoes テーブルへの挿入は自動的にうまく機能しますが、残念ながら、
_Email = StructuralObject.SetValidValue(value, false);
が原因で、電子メールがない場合の挿入に失敗しますIsNullable=false
。自動コード生成は、子孫エンティティを検出して処理するのに十分なほどスマートですが、開発者としての私の心を読んで、電子メールがないときにエンティティの作成をスキップするだけではありません。「エラーがある場合は問題ありません。グラフ内の他のすべてのエンティティを構築しますが、これはスキップします」というコマンドがある場合、それが何であるかわかりません。開発者として、電子メールが入力されていない場合は傍受して介入する必要があります。
残念ながら、自動生成されたdb.People.Attach(people);
も更新に失敗します (述語 ModelState.IsValid が true であっても)。EF は、フォームが送信されたときに子孫の関係をチェックしていないようです。
A referential integrity constraint violation occurred:
The property values that define the referential constraints are not consistent
between principal and dependent objects in the relationship.
Entity Framework を使用し、複数のオブジェクトを編集すると、「参照整合性制約違反」につながる MVC 3 は、 AutoMapper を使用してこの問題を修正することを提案しています。Bengt Berge のAutoMapperの紹介は、すばやく簡単です。People エンティティと ContactInfoes エンティティにまだ含まれていない追加情報がないため、FormsCollection を使用するという提案も適切ではないようです。
そのため、自動生成されたコード (try と catch を使用してエラー レポートと例外ハンドラーを少し試した) では、InfoContact エンティティを作成するか削除するかを何らかの方法で決定する必要があります。
[HttpPost]
public ActionResult Edit(People people)
{
if (ModelState.IsValid)
{
try
{
db.People.Attach(people);
}
catch (Exception ex)
{
return Content("<h1>Error!</h1>"
+ ex.Source + "<br />\n\r"
+ ex.Message + "<br />\n\r"
+ ex.InnerException + "<br />\n\r"
+ ex.StackTrace+ "<br />\n\r"
+ ex.TargetSite + "<br />\n\r"
+ ex.HelpLink + "<br />\n\r"
+ ex.Data + "<br />\n\r"
+ "If this is an emergency call 911.\n\r"
);
}
db.ObjectStateManager.ChangeObjectState(people, EntityState.Modified);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.PersonId = new SelectList(db.ContactInfoes, "PersonId", "Email", people.PersonId);
return View(people);
}
いくつかの補足的な接線
これは垂直的な継承関係ですが、Microsoft のコード ジェネレーターが生成する
ViewBag.PersonId = new SelectList(db.ContactInfoes, "PersonId", "Email", people.PersonId);
このモデルで明確に定義された自己参照シナリオでドロップダウン リストを使用する人がいるでしょうか? 確かに、プログラマーは自動コードを削除するか無視することができますが、そもそも必要がないのになぜ自動コードを生成するのでしょうか?私の粗雑な例外ハンドラーの実装よりもデバッグするためのより良い方法はありますか? 出力はスクワットの平方根を提供します。つまり、何も役に立ちません。私は、Log4net と ELMAH がこれ以上の情報を提供しないと想定しています。利用可能なものを追跡するだけです。
最終的にすべてのViewModelマッピングを実行した後、EFを使用する利点は本当にありますか?
ビューモデル
厳密に型指定されたフォーム処理で IntelliSense を使用するための秘密のソースは AutoMapper 関連であることがわかりました。開発者は ViewModel を追加し、それと People および ContactInfoes エンティティとの間をマップする必要があります。
using System.ComponentModel.DataAnnotations;
public class PCI //PersonContactInfo
{
[Key]
public int PersonId { get; set; }
[Required]
public string Name { get; set; }
// Add Annotation for valid Email or Null
public string Email { get; set; }
}
ViewModel を使用するためのビューの変更
ViewModel を使用するには、cshtml Razor フォーム ファイルの編集と作成に 2 つの変更を加えるだけで済みます。
最初の行は@model MVC3EF4.Models.People
~ から@model MVC3EF4.Models.PCI
そして私の元の修正
@Html.EditorFor(model => model.ContactInfoes.Email)
になる
@Html.EditorFor(model => model.Email)
注: PCI ViewModel を使用して、新しいコントローラーと Razor フォームの構築を開始したわけではありません。
私も DbContext を使用していませんが、EDMX コード生成 = デフォルトです。DbContext を使用する場合は、EDMX コード生成プロパティを none に設定します。
コントローラーの変更
[HttpPost]
public ActionResult Create(PCI pci) //use the ViewModel PCI instead of People
{
if (ModelState.IsValid)
{
// THIS IS WHERE AutoMapper WOULD BE HANDY FOR MAPPING ViewModel
// to the People and ContactInfoes Models
// Map PCI to People
People people = new People();
people.PersonId = pci.PersonId;
people.Name = pci.Name;
// Map PCI to ContactInfoes --if there is an Email
if (pci.Email != null && pci.Email.Length > 3)
{
// KNOWN AND THROWN ????
// Why is there no concurrency error thrown?
// How is this added when PersonId isn't known?
// This isn't standard functional programming. This is how EF builds a graph.
db.AddToContactInfoes(
new ContactInfoes {
PersonId = people.PersonId,
Email = pci.Email
}
);
}
// else don't add ContactInfoes entity/table.
db.People.AddObject(people);
db.SaveChanges();
return RedirectToAction("Index");
}
// Where does a dropdownlist get used? It doesn't for a 1:0..1 relationship.
//ViewBag.PersonId = new SelectList(db.ContactInfoes
//, "PersonId", "Email", people.PersonId);
return View(pci);
}
編集
// GET: /PersonContactInfo/Edit/5
public ActionResult Edit(int id)
{
People people = db.People.Single(p => p.PersonId == id);
// Map People to ViewModel PCI...
PCI pci = new PCI()
{
PersonId = people.PersonId,
Name = people.Name
};
if (people.ContactInfoes != null) { pci.Email = people.ContactInfoes.Email; }
/* why a SelectList in a one-to-one or one-to-none?
* what is to select?
* ViewBag.PersonId = new SelectList(db.ContactInfoes
, "PersonId"
, "Email"
, people.PersonId);
*/
return View(pci);
}
投稿を編集
//
// POST: /PersonContactInfo/Edit/5
[HttpPost]
// THIS DOESN'T WORK
//public ActionResult Edit(People people) //use the ViewModel instead
//public ActionResult Edit(FormCollection col) //No need, everything is available from strongly typed ViewModel
public ActionResult Edit(PCI pci)
{
if (ModelState.IsValid)
{
// THIS DOESN'T WORK
// var people = new People(); --reload what the Person was from the database
People people = db.People.Single(p => p.PersonId == pci.PersonId);
try
{
people.Name = pci.Name;
if (pci.Email != null && pci.Email.Length > 3)
{
if (people.ContactInfoes == null) {
var ci = new ContactInfoes { PersonId = pci.PersonId, Email = pci.Email };
db.AddToContactInfoes(ci);
// THIS DOESN'T WORK
//db.ContactInfoes.Attach(ci) // this causes an error on the next line
// A referential integrity constraint violation occurred: A primary key property that is a part of referential integrity constraint cannot be changed when the dependent object is Unchanged unless it is being set to the association's principal object. The principal object must be tracked and not marked for deletion.
// people.ContactInfoes = ci;
// THIS DOESN'T WORK
//people.ContactInfoesReference.Attach(new ContactInfoes { PersonId = pci.PersonId, Email = pci.Email });
//Attach is not a valid operation when the source object associated with this related end is in an added, deleted, or detached state. Objects loaded using the NoTracking merge option are always detached.
}
else
people.ContactInfoes.Email = pci.Email;
}
else // no user input for Email from form so there should be no entity
{
// THIS DOESN'T WORK
// people.ContactInfoes = null;
// people.ContactInfoesReference = null;
ContactInfoes ci = people.ContactInfoes;
if (ci != null) //if there isn't an ContactInfo record, trying to delete one will cause an error.
db.ContactInfoes.DeleteObject(ci);
/*
* THIS DOESN'T WORK
// this causes a concurrency error
var ci = new ContactInfoes();
ci.PersonId = pci.PersonId;
db.AttachTo("ContactInfoes", ci);
db.ContactInfoes.DeleteObject(ci);
*/
}
// THIS DOESN'T WORK
// db.People.Attach(people);
// doing People people = db.People.Single(p => p.PersonId == pci.PersonId); makes people automatically attached
// The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.
}
catch (Exception ex)
{
return Content("<h1>Error!</h1>"
+ ex.Source + "<br />\n\r"
+ ex.Message + "<br />\n\r"
+ ex.InnerException + "<br />\n\r"
+ ex.StackTrace+ "<br />\n\r"
+ ex.TargetSite + "<br />\n\r"
+ ex.HelpLink + "<br />\n\r"
+ ex.Data + "<br />\n\r"
);
}
db.ObjectStateManager.ChangeObjectState(people, EntityState.Modified);
db.SaveChanges();
return RedirectToAction("Index");
}
//ViewBag.PersonId = new SelectList(db.ContactInfoes, "PersonId", "Email", people.PersonId);
return View(pci);}
SQUEAL [SQL] データ定義言語
-- --------------------------------------------------
-- Entity Designer DDL Script for SQL Server 2005, 2008, and Azure
-- --------------------------------------------------
-- Date Created: 05/31/2012 18:03:38
-- Generated from EDMX file: C:\Users\Jb\Documents\Visual Studio 11\Projects\MVC3EF4\MVC3EF4\Models\PersonContactInfoModel.edmx
-- --------------------------------------------------
SET QUOTED_IDENTIFIER OFF;
GO
USE [MVC3EF4];
GO
IF SCHEMA_ID(N'dbo') IS NULL EXECUTE(N'CREATE SCHEMA [dbo]');
GO
-- --------------------------------------------------
-- Dropping existing FOREIGN KEY constraints
-- --------------------------------------------------
IF OBJECT_ID(N'[dbo].[FK_PersonContactInfo]', 'F') IS NOT NULL
ALTER TABLE [dbo].[ContactInfoes] DROP CONSTRAINT [FK_PersonContactInfo];
GO
-- --------------------------------------------------
-- Dropping existing tables
-- --------------------------------------------------
IF OBJECT_ID(N'[dbo].[ContactInfoes]', 'U') IS NOT NULL
DROP TABLE [dbo].[ContactInfoes];
GO
IF OBJECT_ID(N'[dbo].[People]', 'U') IS NOT NULL
DROP TABLE [dbo].[People];
GO
-- --------------------------------------------------
-- Creating all tables
-- --------------------------------------------------
-- Creating table 'ContactInfoes'
CREATE TABLE [dbo].[ContactInfoes] (
[PersonId] int NOT NULL,
[Email] nvarchar(120) NOT NULL
);
GO
-- Creating table 'People'
CREATE TABLE [dbo].[People] (
[PersonId] int IDENTITY(1,1) NOT NULL,
[Name] nvarchar(50) NOT NULL
);
GO
-- --------------------------------------------------
-- Creating all PRIMARY KEY constraints
-- --------------------------------------------------
-- Creating primary key on [PersonId] in table 'ContactInfoes'
ALTER TABLE [dbo].[ContactInfoes]
ADD CONSTRAINT [PK_ContactInfoes]
PRIMARY KEY CLUSTERED ([PersonId] ASC);
GO
-- Creating primary key on [PersonId] in table 'People'
ALTER TABLE [dbo].[People]
ADD CONSTRAINT [PK_People]
PRIMARY KEY CLUSTERED ([PersonId] ASC);
GO
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
-- Creating foreign key on [PersonId] in table 'ContactInfoes'
ALTER TABLE [dbo].[ContactInfoes]
ADD CONSTRAINT [FK_PersonContactInfo]
FOREIGN KEY ([PersonId])
REFERENCES [dbo].[People]
([PersonId])
ON DELETE CASCADE ON UPDATE NO ACTION;
GO
-- --------------------------------------------------
-- Creating FOREIGN KEY Relationship Documentation
-- --------------------------------------------------
EXEC sys.sp_addextendedproperty @name=N'MS_Description',
@value=N'One-to-One or One-to-None' ,
@level0type=N'SCHEMA',@level0name=N'dbo',
@level1type=N'TABLE',@level1name=N'ContactInfoes',
@level2type=N'CONSTRAINT',@level2name=N'FK_PersonContactInfo'
GO
-- --------------------------------------------------
-- Script has ended
-- --------------------------------------------------