【问题标题】:Entity Framework hard cascade delete实体框架硬级联删除
【发布时间】:2017-01-25 17:39:18
【问题描述】:

我有一个使用实体框架映射的 SQLite 数据库。 有 2 个表:Collections (1:n) Albums。

当我删除一个收藏时,所有相关的相册也必须被删除。 我使用CollectionRepo.Delete(collection); 来实现这一点。它使用以下代码:

public int Delete(Collection entity)
{
    Context.Entry(entity).State = EntityState.Deleted;
    return Context.SaveChanges();
}

问题是:当我执行这段代码时,Context.SaveChanges();给我一个异常:

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

似乎实体框架想要在外键上null 而不是删除条目。但这绝对不是我想要的,因为没有父母的专辑毫无意义(至少在我的用例中)。

我显然可以先手动删除相册,然后删除空集合,但在我看来有点棘手。首先,在我看来,EF 应该足够聪明,可以自己完成以简化代码;其次,如果我与收藏和专辑有几十个关系,我最终会得到一个相当大的、难以维护的、代码库。


集合类

public class Collection
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    public virtual List<Album> Albums { get; set; } = new List<Album>();
}

专辑类

public class Album
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    [ForeignKey("Collection")]
    public long CollectionId { get; set; }

    public virtual Collection Collection { get; set; }
}

DbContext 子类

public class DataEntities : DbContext
{
    public virtual DbSet<Collection> Collections { get; set; }
    public virtual DbSet<Album> Albums { get; set; }

    public DataEntities() : base("name=Connection")
    {
        Configuration.ProxyCreationEnabled = false;
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Album>()
            .HasRequired(a => a.Collection)
            .WithMany(c => c.Albums)
            .HasForeignKey(a => a.CollectionId)
            .WillCascadeOnDelete(true);
        modelBuilder.Entity<Collection>()
            .HasMany(c => c.Albums)
            .WithRequired(a => a.Collection)
            .WillCascadeOnDelete(true);
    }
}

【问题讨论】:

  • 您使用的是哪个版本的 sqlite?听起来这是 sqlite 不支持级联删除 stackoverflow.com/questions/10719425/… 的问题
  • 3.14.2 (System.Data.SQLite 1.0.103)。恕我直言,它似乎与 SQLite 无关,因为我表中的外键允许为空,加上异常堆栈跟踪最深调用是 System.Data.Entity.Core.Objects.ObjectContext.PrepareToSaveChanges(SaveOptions options) 似乎与 SQLite 无关。
  • 您在映射 [Required][ForeignKey] 中将其指定为不可为空,这就是 EF 混淆的原因。

标签: c# entity-framework sqlite


【解决方案1】:

在 EF 中应用分离的对象图修改一直不清楚。这是没有充分理由失败的情况之一。

假设传递给Delete 方法的Collection 实体填充了Albums 集合(至少这是我能够重现异常的方式)。线

Context.Entry(entity).State = EntityState.Deleted;

做两件事:将entityentity.Albums 中的所有Album 对象附加到上下文,将entity 标记为Deleted,以及(注意!)Album 对象为Modified。当您调用 SaveChanges 时,这会导致不正确的行为,并最终生成有问题的异常。

有两种方法(解决方法)可以解决此错误行为。

第一个是将上面的行替换为

Context.Collections.Attach(entity);
Context.Collections.Remove(entity);

效果和上面描述的差不多,不同的是现在相关的Album对象被标记为Deleted,这样SaveChanges就可以成功执行了。

缺点是现在SaveChanges 在删除Collection 的命令之前为每个Album 发出一个DELETE 命令,这是低效且没有多大意义的,因为级联删除会处理该问题完全在数据库中。

第二种选择是保持代码不变,但在附加实体之前清除相关集合:

entity.Albums = null;
Context.Entry(entity).State = EntityState.Deleted;

这允许成功执行SaveChanges,并且它只为实体生成一个DELETE 命令。

缺点是您需要编写额外的代码,并且不要忘记任何支持级联删除的子集合,而不是为需要级联更新的集合(即需要使用 NULL 更新 FK 字段的可选关系) .

选择权在你。

【讨论】:

  • 它有效,感谢您的详细解释,即使在我看来这听起来很不合逻辑/违反直觉(EF,不是你的解释)。如果WillCascadeOnDelete(true) 不能自然处理级联删除,它的作用是什么?你确定你的第一个解决方法是为每个Album 使用deletecommand,而不是像DELETE FROM Albums WHERE CollectionId = xxx 这样的批处理delete
  • 是的,我在回答之前测试了它。它执行 N 个命令,例如 DELETE FROM Albums WHERE Id = @0。最新的 EF6.1.3,SqlServer 数据库。 WillCascadeOnDelete的好处是什么,用来在数据库中创建对应的约束。
  • 但我认为数据库并不重要,因为这似乎是由 EF 基础设施执行的,而 db 提供程序只是生成相应的 SQL 命令。
  • 好的,谢谢,最后一个问题,您在哪里可以跟踪发出的 SQL 命令以及您在哪里调试命令 Context.Entry(entity).State = EntityState.Deleted; 对对象实际执行的操作?
  • (1) Context.Log = Console.WriteLine;Context.Log = s =&gt; Trace.WriteLine(s); (2) 在观看中展开 Context.ChangeTracker.Entries()
【解决方案2】:

根据您的 cmets,您正在映射到预先存在的数据库(EF 没有生成它)。 CascadeOnDelete 只影响数据库的生成。如果数据库没有在表上配置 CascadeOnDelete,那么 EF 在尝试删除时会感到困惑,Sqlite 不遵守。

此外,您将外键的映射设置为不可空且必需(顺便说一句是冗余的),但在数据库中,外键是可空的。 EF 认为它是无效的,因为你告诉了它。

如果您修复了映射(从 CollectionID 属性中删除所需的注释并将其类型更改为 int?而不是仅仅 int,您应该修复您的问题。实际上将 DbContext 类中的映射从 HasRequired 更改为 HasOptional...from它应该在那里工作。

要么改变你的数据库本身的表定义。

【讨论】:

  • 我在 DB 中允许 null 仅用于调试目的。现在我把它放回你NOT NULL,FK 是ON DELETE CASCADE。我不想将我的映射从 HasRequired 更改为 HasOptional,因为在我的设计中,专辑应该始终与收藏相关联(UI 显示收藏然后是专辑,因此如果专辑未与收藏相关联,它将永远不会显示)。
  • 那么在你把它放回不为空并在删除级联时解决问题了吗?
  • 不,那是初始状态,在我更改它以确保它与数据库无关之前。 Ivan Stoev 通过 2 个行为解决了我的问题:首先通过在其答案中提出 2 个解决方法,其次通过指出 DbContext 应该始终是唯一的。
猜你喜欢
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2020-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多