首页 文章

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

提问于
浏览
161

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

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

我不太明白这一行:

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

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

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

代码行可以提炼为:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

16 回答

  • 4

    我已经尝试过这些解决方案和其他许多解决方案,但它们都没有完成 . 由于这是谷歌的第一个答案,我将在这里添加我的解决方案 .

    对我来说效果很好的方法是在提交过程中将关系从图片中删除,因此EF无法搞砸 . 我通过重新找到中的父对象来做到这一点DBContext,并删除它 . 由于重新找到的对象的导航属性都为null,因此在提交期间会忽略子对象的关系 .

    var toDelete = db.Parents.Find(parentObject.ID);
    db.Parents.Remove(toDelete);
    db.SaveChanges();
    

    请注意,这假定外键是使用ON DELETE CASCADE设置的,因此当删除父行时,数据库将清除子项 .

  • 143

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

    您可以删除附加到此类记录的记录集合 .

    order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);
    

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

  • 34

    我刚才遇到了同样的错误 . 我有两个父子关系表,但我在子表的表定义中的外键列上配置了“on delete cascade” . 因此,当我手动删除数据库中的父行(通过SQL)时,它将自动删除子行 .

    但是这在EF中不起作用,这个线程中描述的错误出现了 . 原因是,在我的实体数据模型(edmx文件)中,父表和子表之间的关联属性不正确 . End1 OnDelete 选项配置为 none (我的模型中的"End1"是具有多重性1的结尾) .

    我手动将 End1 OnDelete 选项更改为 Cascade 并且工作正常 . 当我从数据库更新模型时(我有一个数据库第一个模型),我不知道为什么EF无法选择它 .

    为了完整起见,我的删除代码如下所示:

    public void Delete(int id)
        {
            MyType myObject = _context.MyTypes.Find(id);
    
            _context.MyTypes.Remove(myObject);
            _context.SaveChanges(); 
       }
    

    如果我没有定义级联删除,我将不得不在删除父行之前手动删除子行 .

  • 19

    您必须手动清除ChildItems集合并将新项目添加到其中:

    thisParent.ChildItems.Clear();
    thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
    

    之后,您可以调用DeleteOrphans扩展方法,该方法将处理孤立实体(必须在DetectChanges和SaveChanges方法之间调用) .

    public static class DbContextExtensions
    {
        private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();
    
        public static void DeleteOrphans( this DbContext source )
        {
            var context = ((IObjectContextAdapter)source).ObjectContext;
            foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
            {
                var entityType = entry.EntitySet.ElementType as EntityType;
                if (entityType == null)
                    continue;
    
                var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
                var props = entry.GetModifiedProperties().ToArray();
                foreach (var prop in props)
                {
                    NavigationProperty navProp;
                    if (!navPropMap.TryGetValue(prop, out navProp))
                        continue;
    
                    var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                    var enumerator = related.GetEnumerator();
                    if (enumerator.MoveNext() && enumerator.Current != null)
                        continue;
    
                    entry.Delete();
                    break;
                }
            }
        }
    
        private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
        {
            var result = type.NavigationProperties
                .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
                .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
                .Where(v => v.DependentProperties.Length == 1)
                .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);
    
            return new ReadOnlyDictionary<string, NavigationProperty>(result);
        }
    }
    
  • 1

    我使用了Mosh's solution,但对于我来说,如何在代码中首先正确实现组合键并不明显 .

    所以这是解决方案:

    public class Holiday
    {
        [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int HolidayId { get; set; }
        [Key, Column(Order = 1), ForeignKey("Location")]
        public LocationEnum LocationId { get; set; }
    
        public virtual Location Location { get; set; }
    
        public DateTime Date { get; set; }
        public string Name { get; set; }
    }
    
  • 1

    您面临此问题的原因是 compositionaggregation 之间的区别 .

    In composition, the child object is created when the parent is created and is destroyed when its parent is destroyed . 因此它的生命周期由其父母控制 . 例如博客文章及其评论 . 如果删除帖子,则应删除其评论 . 它不存在 . 订单和订单商品也是如此 .

    In aggregation, the child object can exist irrespective of its parent . 如果父对象被销毁,则子对象仍然可以存在,因为它可能稍后被添加到另一个父对象 . 例如:播放列表与该播放列表中的歌曲之间的关系 . 如果删除播放列表,则不应删除歌曲 . 它们可能会添加到不同的播放列表中 .

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

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

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

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

    Solutions:

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

    context.Children.RemoveRange(parent.Children);
    

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

    parent.Children.Clear();
    
  • 1

    发生这种情况是因为子实体被标记为已修改而不是已删除 .

    并且当执行 parent.Remove(child) 时,EF对子实体的修改只是将对其父级的引用设置为 null .

    执行 SaveChanges() 后,您可以在发生异常时检查子窗口's EntityState by typing the following code into Visual Studio'的立即窗口:

    _context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity
    

    其中X应该被删除的实体替换 .

    如果您无权访问 ObjectContext 以执行 _context.ChildEntity.Remove(child) ,则可以通过将外键作为子表上主键的一部分来解决此问题 .

    Parent
     ________________
    | PK    IdParent |
    |       Name     |
    |________________|
    
    Child
     ________________
    | PK    IdChild  |
    | PK,FK IdParent |
    |       Name     |
    |________________|
    

    这样,如果执行 parent.Remove(child) ,EF将正确地将实体标记为已删除 .

  • 2

    您应该手动逐个删除旧子项 thisParent.ChildItems . 实体框架基本上没有例外 .

    Edit

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

    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 然后代码看起来有点不同 . )我还删除了所有存储库抽象以保持简单 .

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

  • 80

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

    • 您从数据库加载 Parent 并获取附加实体

    • 用新的分离子集合替换其子集合

    • 您保存更改但在此操作期间,所有子项都被视为 added 因为EF直到此时才知道它们 . 因此EF尝试将null设置为旧子项的外键并插入所有新子项=>重复行 .

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

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

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

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

    实际上不需要附加(将状态设置为 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();
    
  • 0

    我今天遇到了这个问题,想分享我的解决方案 . 就我而言,解决方案是在从数据库获取Parent之前删除Child项 .

    以前我在下面的代码中这样做 . 然后我会得到这个问题中列出的相同错误 .

    var Parent = GetParent(parentId);
    var children = Parent.Children;
    foreach (var c in children )
    {
         Context.Children.Remove(c);
    }
    Context.SaveChanges();
    

    对我有用的是,首先使用parentId(外键)获取子项,然后删除这些项 . 然后我可以从数据库中获取Parent,此时,它不应再有任何子项,我可以添加新的子项 .

    var children = GetChildren(parentId);
    foreach (var c in children )
    {
         Context.Children.Remove(c);
    }
    Context.SaveChanges();
    
    var Parent = GetParent(parentId);
    Parent.Children = //assign new entities/items here
    
  • 3

    我在几个小时之前遇到了这个问题并尝试了一切,但在我的情况下,解决方案与上面列出的不同 .

    如果您从数据库中使用已检索的实体并尝试修改它的子项,则会发生错误,但如果您从数据库中获取实体的新副本,则不应存在任何问题 . 不要用这个:

    public void CheckUsersCount(CompanyProduct companyProduct) 
     {
         companyProduct.Name = "Test";
     }
    

    用这个:

    public void CheckUsersCount(Guid companyProductId)
     {
          CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
          companyProduct.Name = "Test";
     }
    
  • 0

    我也用Mosh's answer解决了我的问题,我认为PeterB's answer有点因为它使用枚举作为外键 . 请记住,添加此代码后需要添加新的迁移 .

    我还可以推荐此博客文章以获取其他解决方案:

    http://www.kianryan.co.uk/2013/03/orphaned-child/

    码:

    public class Child
    {
        [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
    
        public string Heading { get; set; }
        //Add other properties here.
    
        [Key, Column(Order = 1)]
        public int ParentId { get; set; }
    
        public virtual Parent Parent { get; set; }
    }
    
  • 2

    这种解决方案对我有用:

    Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
    db.Childs.RemoveRange(original.Childs);
    updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
    db.Entry<Parent>(original).CurrentValues.SetValues(updated);
    

    重要的是,这会删除所有记录并再次插入它们 . 但对于我的情况(少于10)这没关系 .

    我希望它有所帮助 .

  • 0

    出现此问题是因为我们尝试删除父表仍然存在子表数据 . 我们在级联删除的帮助下解决了这个问题 .

    In model Create method in dbcontext class.

    modelBuilder.Entity<Job>()
                    .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                    .WithRequired(C => C.Job)
                    .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
                modelBuilder.Entity<Sport>()
                    .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                      .WithRequired(C => C.Sport)
                      .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);
    

    之后,在我们的API调用中

    var JobList = Context.Job                       
              .Include(x => x.JobSportsMappings)                                     .ToList();
    Context.Job.RemoveRange(JobList);
    Context.SaveChanges();
    

    Cascade delete 选项使用此简单代码删除父级以及父级相关子表 . 以这种简单的方式尝试 .

    删除用于删除数据库中记录列表的范围谢谢

  • 66

    如果您使用AutoMapper同一个类上的Entity Framework,你可能会遇到这个问题 . 例如,如果你的 class 是

    class A
    {
        public ClassB ClassB { get; set; }
        public int ClassBId { get; set; }
    }
    
    AutoMapper.Map<A, A>(input, destination);
    

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

    将AutoMapper设置为忽略 ClassB 属性 .

    cfg.CreateMap<A, A>()
         .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
    
  • 9

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

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

    您必须使用“identifying relationship”正确define the parent - child relationship .

    如果执行此操作,Entity Framework将知道父对象已标识子对象,因此它必须是"cascade-delete-orphans"情况 .

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

    thisParent.ChildItems.Clear();
    thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
    

    而不是完全替换列表 .

    UPDATE

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

相关问题