在一对多的关系中删除父级及其子级

11

我有一个使用Entity Framework 5.0和Sql Server CE 4.0的.NET 4.0应用程序。

我有两个实体,它们之间有一对多(父/子)关系。我已将其配置为在删除父项时级联删除,但出于某种原因它似乎不起作用。

这是我的实体的简化版本:

    public class Account
    {
        public int AccountKey { get; set; }
        public string Name { get; set; }

        public ICollection<User> Users { get; set; }
    }

    internal class AccountMap : EntityTypeConfiguration<Account>
    {
        public AccountMap()
        {
            this.HasKey(e => e.AccountKey);
            this.Property(e => e.AccountKey).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            this.Property(e => e.Name).IsRequired();
        }
    }


    public class User
    {
        public int UserKey { get; set; }
        public string Name { get; set; }

        public Account Account { get; set; }
        public int AccountKey { get; set; }
    }

    internal class UserMap : EntityTypeConfiguration<User>
    {
        public UserMap()
        {
            this.HasKey(e => e.UserKey);
            this.Property(e => e.UserKey).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            this.Property(e => e.Name).IsRequired();


            this.HasRequired(e => e.Account)
                .WithMany(e => e.Users)
                .HasForeignKey(e => e.AccountKey);
        }
    }

    public class TestContext : DbContext
    {
        public TestContext()
        {
            this.Configuration.LazyLoadingEnabled = false;
        }

        public DbSet<User> Users { get; set; }
        public DbSet<Account> Accounts { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.Conventions.Remove<StoreGeneratedIdentityKeyConvention>();
            modelBuilder.LoadConfigurations();
        }

    }

连接字符串:

  <connectionStrings>
    <add name="TestContext" connectionString="Data Source=|DataDirectory|\TestDb.sdf;" providerName="System.Data.SqlServerCe.4.0" />
  </connectionStrings>

我的应用程序工作流程的简化版本:

static void Main(string[] args)
{
    try
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<TestContext>());
        using (var context = new TestContext())
            context.Database.Initialize(false);

        Account account = null;
        using (var context = new TestContext())
        {
            var account1 = new Account() { Name = "Account1^" };
            var user1 = new User() { Name = "User1", Account = account1 };

            context.Accounts.Add(account1);
            context.Users.Add(user1);

            context.SaveChanges();

            account = account1;
        }

        using (var context = new TestContext())
        {
            context.Entry(account).State = EntityState.Deleted;
                    context.SaveChanges();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }

    Console.WriteLine("\nPress any key to exit...");
    Console.ReadLine();
}

尝试删除父实体时,会抛出以下错误:

  

由于外键属性中有一个或多个非空值,因此无法更改关系。 当关系发生更改时,相关的外键属性将设置为空值。 如果外键不支持null值,则必须定义新关系,将外键属性指定为另一个非null值或删除无关对象。

我相信我的关系配置没有问题(遵循文档)。我还搜索了有关删除分离实体的指南

我真的无法理解为什么删除不起作用。 我想避免加载所有子项,逐个删除它们,然后再删除父项,因为一定有更好的解决方案。

2个回答

14

将实体的状态设置为Deleted并调用DbSet<T>.Remove与此实体不同。

区别在于,仅设置状态只更改根实体(传递到context.Entry中的实体)的状态为Deleted,而不更改相关实体的状态,而Remove在关系配置有级联删除时会更改相关实体的状态。

是否会出现异常实际上取决于子级(全部还是部分)是否连接到上下文中。这导致了一种难以理解的行为:

  • 如果您调用Remove,无论子项是否已加载,都不会引发异常。但仍然存在差异:
    • 如果子项已连接到上下文,则EF将为每个已连接的子项生成一个DELETE语句,然后是父项(因为Remove将所有子项都标记为Deleted
    • 如果子项未连接到上下文,则EF只会将DELETE语句发送到数据库以删除父项,并且由于启用了级联删除,数据库将同时删除子项。
  • 如果将根实体的状态设置为Deleted,则可能会引发异常:
    • 如果子项已连接到上下文,则它们的状态不会设置为Deleted,EF将抱怨您试图在未删除从属项(子项)或至少未将其外键设置为另一个未处于Deleted状态的根实体的必需关系中删除主要项(根实体)。这就是您遇到的异常情况:account是根,user1account的依赖项,并且调用context.Entry(account).State = EntityState.Deleted;将在上下文中以状态Unchanged附加user1(或者改变检测中的SaveChanges将完成此操作,我不确定)。user1account.Users集合的一部分,因为关系修复将它添加到第一个上下文的集合中,尽管您没有在代码中显式添加它。
    • 如果没有子项连接到上下文,则将根状态设置为Deleted将向数据库发送DELETE语句,并且再次,在数据库中进行级联删除将同时删除子项。这可以正常工作而不引发异常。例如,在进入第二个上下文之前或在将状态设置为Deleted之前,可以通过设置account.Users=null来使您的代码生效。

在我看来,使用Remove更好...

using (var context = new TestContext())
{
    context.Accounts.Attach(account);
    context.Accounts.Remove(account);
    context.SaveChanges();
}

...显然是首选的方式,因为Remove的行为更符合具有级联删除要求关系的预期(这在您的模型中是这种情况)。手动状态更改的行为依赖于其他实体的状态,使其更难使用。我认为这只适用于特殊情况的高级用法。

这种差异并不广为人知或记录。我很少看到相关帖子。目前我唯一能找到的一个是Zeeshan Hirani的这篇文章


1
非常有启发性,@Slauma!在发布问题之前,我搜索了很多线索,但我没有找到你提到的帖子。谢谢。 - Arthur Nunes
1
看起来很多人在尝试使用EF进行父子操作(如插入、更新和删除)时遇到了很多问题,每次我读更多关于EF的内容,就越感到失望。 - Junior Mayhé

1
我会尝试一种稍微不同的方法,奇怪的是,它起作用了。如果我替换这段代码:
using (var context = new TestContext())
{
    context.Entry(account).State = EntityState.Deleted;
    context.SaveChanges();
}

通过这个代码片段:
using (var context = new TestContext())
{
    context.Entry(account).State = EntityState.Unchanged;
    context.Accounts.Remove(account);
    context.SaveChanges();
}

它可以正常工作,没有其他问题。不确定这是否是一个 bug 还是我漏掉了什么。我真的很希望能够解决这个问题,因为我非常确定第一种方式(EntityState.Deleted)是推荐的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接