由于一个或多个外键属性不可为空,因此无法更改关系

jaf*_*ffa 179 entity-framework entity-framework-4.1

当我在一个实体上使用GetById()然后将子实体的集合设置为来自MVC视图的新列表时,我收到此错误.

操作失败:无法更改关系,因为一个或多个外键属性不可为空.当对关系进行更改时,相关的外键属性将设置为空值.如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象.

我不太明白这一行:

由于一个或多个外键属性不可为空,因此无法更改关系.

为什么要更改2个实体之间的关系?它应该在整个应用程序的整个生命周期内保持不变.

发生异常的代码很简单,即将集合中已修改的子类分配给现有父类.这有望满足删除子类,添加新类和修改的需要.我原以为Entity Framework会处理这个问题.

代码行可以提炼为:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
Run Code Online (Sandbox Code Playgroud)

Sla*_*uma 153

您应该thisParent.ChildItems手动逐个删除旧子项.实体框架不会为您做到这一点.它最终无法决定你想要对旧的子项目做什么 - 如果你想要扔掉它们或者你想保留它们并将它们分配给其他父实体.您必须告诉实体框架您的决定.但是,您必须做出这两个决定中的一个,因为子实体不能在没有引用数据库中的任何父项的情况下独自生活(由于外键约束).这基本上就是异常所说的.

编辑

如果可以添加,更新和删除子项,我会怎么做:

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

注意:这未经过测试.假设子项集合是类型的ICollection.(我通常有IList,然后代码看起来有点不同.)我也删除了所有存储库抽象,以保持简单.

我不知道这是否是一个很好的解决方案,但我相信必须在这些方面做一些艰苦的工作来处理导航集合中的各种变化.我也很高兴看到一种更简单的方法.

  • 我会在 foreach 中检索 originalChildItem 时添加一个条件: ...Where(c => c.ID == childItem.ID && c.ID != 0) 否则它将返回新添加的子项,如果 childItem.ID == 0。 (2认同)

Mos*_*osh 106

您面临这种情况的原因是由于构成聚合之间的差异.

在组合中,子对象在创建父对象时创建,在其父对象被销毁时被销毁.因此它的生命周期由其父母控制.例如博客文章及其评论.如果删除帖子,则应删除其评论.对不存在的帖子发表评论是没有意义的.订单和订单商品也是如此.

在聚合中,子对象可以存在而与其父对象无关.如果父对象被销毁,则子对象仍然可以存在,因为它可能会在以后添加到其他父对象.例如:播放列表与该播放列表中的歌曲之间的关系.如果删除播放列表,则不应删除歌曲.它们可能会添加到不同的播放列表中.

实体框架区分聚合和组合关系的方式如下:

  • 对于组合:它期望子对象具有复合主键(ParentID,ChildID).这是设计的,因为孩子的ID应该在他们父母的范围内.

  • 对于聚合:它期望子对象中的外键属性可以为空.

因此,您遇到此问题的原因是您在子表中设置主键的原因.它应该是复合的,但事实并非如此.因此,实体框架将此关联视为聚合,这意味着,当您删除或清除子对象时,它不会删除子记录.它只是删除关联并将相应的外键列设置为NULL(因此这些子记录以后可以与不同的父关联).由于您的列不允许NULL,因此您将获得您提到的异常.

解决方案:

1-如果您有充分理由不想使用复合键,则需要显式删除子对象.这可以比之前建议的解决方案更简单:

context.Children.RemoveRange(parent.Children);
Run Code Online (Sandbox Code Playgroud)

2-否则,通过在子表上设置正确的主键,您的代码看起来会更有意义:

parent.Children.Clear();
Run Code Online (Sandbox Code Playgroud)

  • 我发现这个解释最有帮助. (9认同)
  • 组合与聚合的良好解释以及实体框架如何与之相关. (7认同)

Lad*_*nka 70

这是一个非常大的问题.您的代码中实际发生的是:

  • Parent从数据库加载并获取附加的实体
  • 您将其子集合替换为新的独立子集合
  • 您保存更改但在此操作期间,所有孩子都被视为添加,因为EF直到这时才知道他们.因此EF尝试将null设置为旧子项的外键并插入所有新子项=>重复行.

现在解决方案真的取决于你想做什么以及你想怎么做?

如果您使用的是ASP.NET MVC,则可以尝试使用UpdateModel或TryUpdateModel.

如果您只想手动更新现有子项,您可以执行以下操作:

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

实际上不需要附加(将状态设置为Modified也将附加实体)但我喜欢它,因为它使过程更加明显.

如果要修改现有项,删除现有项并插入新项,则必须执行以下操作:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

  • 父级.ChildItems.Remove(子级); context.Childs.Remove(child); 此双重删除修复了可能出现的问题,谢谢。为什么我们需要两者都删除?为什么仅仅从parent.ChildItems中删除是不够的,因为孩子只作为孩子生活? (2认同)

Gre*_*tle 39

我发现这个答案对同样的错误更有帮助.当您删除时,EF似乎不喜欢它,它更喜欢删除.

您可以删除附加到这样的记录的记录集合.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);
Run Code Online (Sandbox Code Playgroud)

在该示例中,附加到订单的所有详细记录都将其状态设置为"删除".(准备添加更新的详细信息,作为订单更新的一部分)


And*_*uus 19

我不知道为什么其他两个答案如此受欢迎!

我相信你认为ORM框架应该处理它是正确的 - 毕竟,这是它承诺提供的.否则,您的域模型会因持久性问题而损坏.如果正确设置级联设置,NHibernate会愉快地管理它.在实体框架中,它们也有可能,它们只是希望您在设置数据库模型时遵循更好的标准,特别是当他们必须推断应该执行的级联时:

您必须使用" 标识关系 " 正确定义父子关系.

如果这样做,Entity Framework知道子对象是由父对象标识的,因此它必须是"级联 - 删除 - 孤立"情况.

除了以上,你可能需要(来自NHibernate的经验)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
Run Code Online (Sandbox Code Playgroud)

而不是完全替换列表.

UPDATE

@Slauma的评论提醒我,分离的实体是整体问题的另一部分.要解决这个问题,您可以采用自定义模型绑定器的方法,通过尝试从上下文加载模型来构建模型.这篇博文显示了我的意思的一个例子.


jsg*_*pil 9

如果您在同一个类上使用AutoMapper和Entity Framework,则可能会遇到此问题.例如,如果你的班级是

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);
Run Code Online (Sandbox Code Playgroud)

这将尝试复制这两个属性.在这种情况下,ClassBId不可为空.由于AutoMapper将复制destination.ClassB = input.ClassB;这将导致问题.

将AutoMapper设置为Ignore ClassB属性.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
Run Code Online (Sandbox Code Playgroud)


Eku*_*kus 5

我有同样的问题,但我知道它在其他情况下工作正常,所以我将问题简化为:

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
Run Code Online (Sandbox Code Playgroud)
  • OtherRelatedItems有一个复合主键(parentId + 一些本地列)并且工作正常
  • ProblematicItems有自己的单列主键,parentId只是一个 FK。这导致了 Clear() 之后的异常。

我所要做的就是让 ParentId 成为复合 PK 的一部分,以表明没有父母就不能存在孩子。我使用了 DB-first 模型,添加了 PK并将 parentId 列标记为 EntityKey(因此,我不得不在 DB 和 EF 中更新它 - 不确定单独的 EF 是否足够)。

我使 RequestId 成为 PK 的一部分 然后更新 EF 模型,并将其他属性设置为实体键的一部分

仔细想想,这是一个非常优雅的区别,EF 使用它来确定没有父级的子级是否“有意义”(在这种情况下,Clear() 不会删除它们并抛出异常,除非您将 ParentId 设置为其他/特殊的),或者 - 就像在原始问题中一样 - 我们希望一旦从父项中删除这些项目就会被删除。

  • +1很好的答案,我今天遇到了这个问题,但无法解决。遵循您的解决方案(使 ID 和外键列成为复合 PK,我的 .Clear() 操作终于成功了。谢谢。 (2认同)

归档时间:

查看次数:

150645 次

最近记录:

6 年,4 月 前