首页 > 解决方案 > 删除子项时出现“实体之间的关联已被切断”错误,关系根据复合键定义

问题描述

所以前言:我有一对模型,一个被定义为“父”的模型,它有自己的 ID、属性和子组件的字段:

public class SearchDesign
{
    [Key]
    [DataMember]
    public int Id {get; set;}
    
    // ... more properties
    
    [DataMember]
    public virtual ICollection<SearchComponent> SearchComponents { get; set; }
}

组件本身是 的子级SearchDesign,并且设置为也可以表示为树。

public class SearchComponent
{
    [Key]
    [DataMember]
    public int Id { get; set; }
    [Key]
    [DataMember]
    public int SearchDesignID { get; set; }
    [Required]
    [DataMember]
    public int Order { get; set; }
    [DataMember]
    public int? ParentID { get; set; }

    // ... more properties

    [DataMember]
    public virtual SearchDesign SearchDesign { get; set; }
    [DataMember]
    public virtual SearchComponent ParentComponent { get; set; }
    [DataMember]
    public virtual ICollection<SearchComponent> ChildComponents { get; set; }
}

模型的这些特定部分是在 DBContext 中设置的,如下所示......

modelBuilder.Entity<SearchComponent>()
    .HasKey(sc => new { sc.Id, sc.SearchDesignID });

modelBuilder.Entity<SearchComponent>()
    .HasMany(parent => parent.ChildComponents)
    .WithOne(child => child.ParentComponent)
    .HasForeignKey(parent => new { parent.ParentID, parent.SearchDesignID })
    .IsRequired(false)
    .OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<SearchComponent>()
    .HasOne(sc => sc.SearchDesign)
    .WithMany(sd => sd.SearchComponents)
    .HasForeignKey(sc => sc.SearchDesignID)
    .IsRequired(false);         

因此,有一点我尝试通过 API 执行更新函数,该 API 调用一个函数,该函数将覆盖我们数据库中现有实体的特征。在执行此操作的过程中,我们从设计中“删除”相关组件,然后以反映在来自请求的输入中的状态重新添加它们:

//Update case.
design = design.Clone(); //function that un-links the entity from EF core by using Newtonsoft to de-serialize and then re-serialize it.

SearchDesign existing = LoadDesign(design.Id);
if (existing == null)
{
    throw new ArgumentOutOfRangeException();
}
else
{
    existing.Name = design.Name;
    
    ...

    //Remove and re-add.
    DBContext.SearchComponents.RemoveRange(existing.SearchComponents); //THIS throws the error.
    foreach (var component in design.SearchComponents)
    {
        component.SearchDesign = existing;
    }
    DBContext.SearchComponents.AddRange(design.SearchComponents);
}               

问题是,当我尝试RemoveRange(我重申)尝试删除依赖元素时,它会引发此错误:

System.InvalidOperationException:实体“SearchDesign”和“SearchComponent”与键值“{SearchDesignID: 7}”之间的关联已被切断,但该关系被标记为“必需”或隐式需要,因为外键不可为空. 如果在切断所需关系时应删除依赖/子实体,则设置关系以使用级联删除。

我的直觉让我相信这可能与SearchComponent结构上的父/子循环有关,但错误似乎表明这是与设计/组件链接有关的问题?如果情况确实如此,那么这个错误就没有任何意义,因为我正在尝试删除依赖/子实体,而不是父实体,并且我不能完全将SearchDesignIDget 设置为 null,因为这是元素键的一部分。

EF Core 专家的任何建议将不胜感激,谢谢!

标签: entity-framework-core

解决方案


我的直觉让我相信这可能与 SearchComponent 结构上的父/子循环有关

事实证明这是正确的。主要问题是 RootDBContext 定义的这一部分:

modelBuilder.Entity<SearchComponent>()
    .HasMany(parent => parent.ChildComponents)
    .WithOne(child => child.ParentComponent)
    .HasForeignKey(parent => new { parent.ParentID, parent.SearchDesignID })
    .IsRequired(false)
    .OnDelete(DeleteBehavior.Restrict);

限制删除行为是导致错误生成的原因。将其更改为DeleteBehavior.Cascade导致问题停止发生......在单元测试中。

这产生了另一个问题,其中由于更改而创建的迁移无法应用于我的本地环境数据库,因为迁移脚本会提供有关删除级联循环的错误。问题是,我研究过的关于在 EF Core 中实现自引用树结构的其他在线资源使用了该DeleteBehavior.Restrict设置,这就是原因。

因此,真正的解决方案是手动完成工作 - 因为DbSet.RemoveRange通常用于删除不同实体之间的非递归关系,它可能会产生错误,因为它无法识别我的哪些SearchComponent实体是孩子,哪些是孩子是父母。使用非常基本的深度优先树遍历似乎绕过了依赖问题:

private void DeleteTheTree(ICollection<SearchComponent> componentsToDelete)
{
    foreach (SearchComponent component in componentsToDelete)
    {
        if (component.ChildComponents != null)
        {
            DeleteTheTree(component.ChildComponents);
        }
        DBContext.SearchComponents.Remove(component);
    }
}

推荐阅读