首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >无法更改关系,因为一个或多个外键属性不可为空

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

Stack Overflow用户
提问于 2011-04-04 21:13:28
回答 21查看 178.4K关注 0票数 206

当我对一个实体执行GetById()操作,然后将子实体的集合设置为来自MVC视图的新列表时,我得到了这个错误。

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

我不太明白这句话:

无法更改关系,因为一个或多个外键属性不可为non。

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

发生异常的代码只是简单地将集合中修改过的子类分配给现有的父类。这将有望满足删除子类、添加新类和修改类的需要。我本以为Entity Framework会处理这个问题。

代码行可以提炼为:

代码语言:javascript
复制
var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
EN

回答 21

Stack Overflow用户

回答已采纳

发布于 2011-04-04 23:45:10

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

编辑

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

代码语言:javascript
复制
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();
}

注意:这是未经测试的。假设子项集合的类型为ICollection。(我通常使用IList,然后代码看起来有点不同。)为了简单起见,我还去掉了所有的存储库抽象。

我不知道这是否是一个好的解决方案,但我相信必须沿着这些路线做一些艰苦的工作,以处理导航集合中的各种更改。我也很高兴看到一种更简单的方法。

票数 169
EN

Stack Overflow用户

发布于 2015-10-07 11:21:56

您面临这种情况的原因是由于组合和聚合之间的差异。

在组合中,创建父对象时创建子对象,销毁其父对象时销毁其子对象。所以它的生命周期是由它的父进程控制的。例如,一篇博客文章及其评论。如果一个帖子被删除了,它的评论也应该被删除。对一个不存在的帖子进行评论是没有意义的。订单和订单项也是如此。

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

Entity Framework区分聚合关系和组合关系的方式如下:

用于组合的

  • :它要求子对象有一个组合主键(ParentID,ChildID)。这是经过设计的,因为孩子的ID应该在其父母的范围内。用于聚合的
  • :它要求子对象中的外键属性可以为空。

因此,出现这个问题的原因是因为您在子表中设置主键的方式。它应该是复合的,但它不是。因此,实体框架将这种关联视为聚合,这意味着,当您删除或清除子对象时,它不会删除子记录。它只需删除关联并将相应的外键列设置为NULL (这样,这些子记录以后就可以与不同的父记录关联)。由于您的列不允许使用NULL,因此会出现您提到的异常。

解决方案:

1-如果您有很强的理由不想使用组合键,则需要显式删除子对象。这可以比前面建议的解决方案更简单地完成:

代码语言:javascript
复制
context.Children.RemoveRange(parent.Children);

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

代码语言:javascript
复制
parent.Children.Clear();
票数 129
EN

Stack Overflow用户

发布于 2011-04-05 03:49:55

这是一个非常大的问题。在您的代码中实际发生的情况是:

  • 你从数据库加载Parent并获得一个附加的实体
  • 你用分离的孩子的新集合替换它的子集合
  • 你保存更改,但是在这个操作中所有的孩子都被认为是添加的,因为EF直到现在才知道它们。因此EF尝试将旧子项的外键设置为null,并插入所有新的子项=>重复行。

现在,解决方案实际上取决于你想要做什么,以及你想怎么做?

如果你正在使用ASP.NET MVC,你可以尝试使用UpdateModel or TryUpdateModel。

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

代码语言:javascript
复制
foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

附加实际上不是必需的(将状态设置为Modified也会附加实体),但我喜欢它,因为它使这个过程更加明显。

如果你想修改现有的,删除现有的并插入新的孩子,你必须这样做:

代码语言:javascript
复制
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();
票数 76
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/5538974

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档