2

为什么微软的实体框架会因为一个基本简单的单一外键、两个表名称电子邮件在一个表单中失败......我该如何解决?

我们都听说过飞行常客计划。加入我受 Edison 启发的频繁失败计划,学习如何不使用 EF。查看我尝试过的代码以及下面的相应错误。

Morteza Manavi描述了这种一对一的主键关联或共享主键问题。Telerik 的OpenAccess ORM 称之为“垂直继承”。EF 没有像 Ruby On Rails 的ActiveRecord belongs_to这样的一致术语或属性。

如果您想快速制作原型并玩...

2 个表格,4 个字段总计

教学用例

在这种情况下没有很多。如果您正在寻找一对多或多对多的答案,对不起,您来错地方了。多重性是 1 人对 0..1 封电子邮件。

如果用户在 Razor 表单中将 Email 留空,则 Person 和 ContactInfoes 之间的关系应该是 1:0(即一对无 [12n1])且没有 ContactInfoes 记录。

2 个实体,0..1 多重性否则,ContactInfoes 与 People 具有 1:1(即一对一 [121])的关系。或者,人员记录与 ContactInfoes 记录具有 1:1 的关系。

我已经使用 squeal [SQL] 多年并且喜欢图片,但是上面的 EDMX 屏幕截图并没有提供背景中正在发生的事情的完整图片。为了让一对一或一的 EF 和Code Firsterers通过为每个引用另一个实体的实体创建一个虚拟ICollection来增加复杂性,如下面的 DbContext Code First 模型中所列。

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 表单。

具有关联 ContactInfoes.Email 输入字段的人员表单

失败失败失败

尽管在有电子邮件时自动插入到 ContactInfoes 表中效果很好,因为 EF 构建了完整的实体图 :) ,不幸的是,

_Email = StructuralObject.SetValidValue(value, false);

插入失败,因为没有电子邮件,因为IsNullable=false. 自动代码生成足够智能,可以检测和处理后代实体,但作为开发人员并没有读懂我的想法,并且在没有电子邮件时简单地跳过创建实体。如果有“如果有错误,那没关系,在图中构建所有其他实体,但跳过这个”命令我不知道它是什么。作为开发人员,当没有输入电子邮件时,我必须拦截和插入。

不幸的是,自动生成db.People.Attach(people);的更新也会失败(即使谓词 ModelState.IsValid 为真)。提交表单时,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 导致“违反参照完整性约束”建议使用 AutoMapper 来解决这个问题,这对于整个解决方案中的 3 属性总数来说有点过头了。Bengt Berge 的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 + "<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);
}

一些旁注切线

  1. 虽然这是微软代码生成器产生的垂直继承关系,ViewBag.PersonId = new SelectList(db.ContactInfoes, "PersonId", "Email", people.PersonId); 为什么有人会在这个模型中明确定义的自引用场景中使用下拉列表?当然,程序员可以删除自动代码,或者忽略它,但为什么在不需要的时候首先生成它呢?

  2. 有没有比我粗略的异常处理程序实现更好的调试方法?输出提供了 squat 的平方根,即没有任何用处。我假设 Log4net 和 ELMAH 不会提供更多信息,只是跟踪可用的信息。

  3. 最终完成所有 ViewModel 映射后,使用 EF 真的有优势吗?

视图模型

事实证明,在强类型表单处理中使用 IntelliSense 的秘诀是 AutoMapper 相关的,因为开发人员需要添加一个 ViewModel 并在它与 People 和 ContactInfoes 实体之间进行映射,因此附注切线 #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 只需要在 Edit 和 Create cshtml Razor 表单文件中进行两次更改。

第一行从@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 代码生成属性设置为无。

控制器修改

[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
-- --------------------------------------------------
4

0 回答 0