为什么Microsoft的实体框架失败了一个基本上简单的单个外键,一个表中的两个表 NameEmail 失败...我该如何修复它?

我们都听说过飞行常客计划 . 加入我的爱立信启发的频繁失败计划,了解如何不使用EF . 看看我试过的代码和下面的相应错误 .

Morteza Manavi描述了这个One-to-One Primary Key Association or Shared Primary Key问题 . Telerik的 OpenAccess ORM称之为“Vertical Inheritanc e” . EF没有't have a consistent term or property like Ruby On Rails' ActiveRecord belongs_to .

如果要快速原型化和播放,下面将包含用于构建此SQL Server图中显示的两个表,外键和唯一约束的T-SQL脚本...

2 Tables, 4 Fields Total

教学用例

在这种情况下没有多少 . 如果您正在寻找1对多或多对多的答案,抱歉,您的位置错误 . 多重性是1人到0..1电子邮件 .

如果用户以Razor形式留下电子邮件空白,则Person和ContactInfoes之间的关系应该是1:0(即,一对一[12n1]),没有ContactInfoes记录 .

2 Entities, 0..1 Multiplicity
否则ContactInfoes与People有1:1的关系(即一对一[121]) . 或者,People记录与ContactInfoes记录具有1:1的关系 .

我提供了背景中正在发生的事情的完整画面 . 为了使一对一或一个EF和Code Firsterers通过为引用下面的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和代码生成器从People模型创建的视图 - 可以来自EDMX图,T4 .tt文件或Code First类 .

使用EF为Noob做直观的事情就是将 @Html.EditorFor(model => model.**ContactInfoes.Email**) 添加到Create.cshtml和Edit.cshtml自动生成的Razor表单中 .

Person Form with Associated ContactInfoes.Email input field

失败失败失败

虽然在有电子邮件时插入到ContactInfoes表中会自动运行,因为EF会构建完整的实体图:),不幸的是,

_Email = StructuralObject.SetValidValue(value, false);

没有电子邮件的插入失败,因为 IsNullable=false . 自动代码生成足够智能,可以检测和处理后代实体,但不作为开发人员阅读我的想法,只是在没有电子邮件时跳过创建实体 . 如果有一个"if there is an error, that's OK, build all the other entities in the graph, but skip this one"命令,我不知道它是什么 . 作为开发人员,我必须在没有输入电子邮件时拦截和插入 .

不幸的是,自动生成的 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.

MVC 3 with Entity Framework and Editing Multiple Objects leads to "referential integrity constraint violation"建议使用AutoMapper来解决这个问题,在整个解决方案中对于3个属性的总数来说有点过头了 . Bengt Berge的Intro to AutoMapper快速而简单 . 使用FormsCollection的建议不要使用People和ContactInfoes实体中尚未包含的任何其他信息 .


因此,这里显示的自动生成的代码(我尝试使用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 + "
\n\r" + ex.Message + "
\n\r" + ex.InnerException + "
\n\r" + ex.StackTrace+ "
\n\r" + ex.TargetSite + "
\n\r" + ex.HelpLink + "
\n\r" + ex.Data + "
\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真的有优势吗?

ViewModel

事实证明,在强类型表单处理中使用IntelliSense的秘诀是与AutoMapper相关,因为开发人员需要在它与People和ContactInfoes实体之间添加一个ViewModel和map,因此侧面注意切线#3 .

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表单文件中进行两处更改 .

第一行从 @model MVC3EF4.Models.People@model MVC3EF4.Models.PCI

和我原来的修改

@Html.EditorFor(model => model.ContactInfoes.Email)

@Html.EditorFor(model => model.Email)

注意:我没有开始使用PCI ViewModel构建我的新Controller和Razor Forms .
我也没有使用DbContext,但是EDMX代码生成=默认 . 如果要使用DbContext设置EDMX代码生成属性=无 .

控制器修改

[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 + "
\n\r" + ex.Message + "
\n\r" + ex.InnerException + "
\n\r" + ex.StackTrace+ "
\n\r" + ex.TargetSite + "
\n\r" + ex.HelpLink + "
\n\r" + ex.Data + "
\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
-- --------------------------------------------------