DELETE语句与具有Entity Framework的SAME TABLE REFERENCE约束冲突

我有一个带有自引用的表,其中ParentId是ID(PK)的FK。
使用EF(代码优先),我建立了如下关系:

this.HasOptional(t => t.ParentValue) .WithMany(t => t.ChildValues) .HasForeignKey(t => t.ParentId); 

当我尝试删除子节点及其父节点时,DELETE命令对数据库的EF问题不按我预期的顺序 – 它首先尝试删除父节点。

我意识到我在这里有几个选项(我都不喜欢):

  1. 首先删除子记录,执行完整保存/提交,然后删除父记录。 由于我的模型的复杂性和维护它的逻辑,这不是一个选项 – 不能随时发出多个提交命令。
  2. 在删除任何内容之前解散关系。 这似乎是一个更明智的解决方案,但同样,我必须在DELETE之前使用UPDATE语句发出单独的提交。 我想避免多次保存/提交调用。
  3. 在删除父记录之前,使用触发器删除子项。 但我想尽可能避免触发器及其有问题的性质。

所以问题是..有没有办法在父记录之前强制删除子项? 也许我错过了一种明确的方式告诉EF它需要在父母之前照顾这些孩子? 也许有一种方法可以指示EF按ID的降序删除? 我不知道……想法?

我意识到答案是一岁,但我发现它不完整。 在我看来,自引用表用于表示任意深度。

例如,请考虑以下结构:

 /* * earth * europe * germany * ireland * belfast * dublin * south america * brazil * rio de janeiro * chile * argentina * */ 

答案并没有解决如何从上面的结构中删除地球或欧洲的问题。

我提交以下代码作为替代方案(由Slauma提供的答案的修改,谁做得很好btw)。

在MyContext类中,添加以下方法:

 public void DeleteMyEntity(MyEntity entity) { var target = MyEntities .Include(x => x.Children) .FirstOrDefault(x => x.Id == entity.Id); RecursiveDelete(target); SaveChanges(); } private void RecursiveDelete(MyEntity parent) { if (parent.Children != null) { var children = MyEntities .Include(x => x.Children) .Where(x => x.ParentId == parent.Id); foreach (var child in children) { RecursiveDelete(child); } } MyEntities.Remove(parent); } 

我使用以下类使用代码优先填充数据:

 public class TestObjectGraph { public MyEntity RootEntity() { var root = new MyEntity { Name = "Earth", Children = new List { new MyEntity { Name = "Europe", Children = new List { new MyEntity {Name = "Germany"}, new MyEntity { Name = "Ireland", Children = new List { new MyEntity {Name = "Dublin"}, new MyEntity {Name = "Belfast"} } } } }, new MyEntity { Name = "South America", Children = new List { new MyEntity { Name = "Brazil", Children = new List { new MyEntity {Name = "Rio de Janeiro"} } }, new MyEntity {Name = "Chile"}, new MyEntity {Name = "Argentina"} } } } }; return root; } } 

我使用以下代码保存到我的数据库:

 ctx.MyEntities.Add(new TestObjectGraph().RootEntity()); 

然后像这样调用删除:

 using (var ctx = new MyContext()) { var parent = ctx.MyEntities .Include(e => e.Children) .FirstOrDefault(); var deleteme = parent.Children.First(); ctx.DeleteMyEntity(deleteme); } 

这导致我的数据库现在具有如下结构:

  /* * earth * south america * brazil * rio de janeiro * chile * argentina * */ 

欧洲及其所有儿童被删除的地方。

在上面,我指定了根节点的第一个子节点,以certificate使用我的代码可以递归地从层次结构中的任何位置删除节点及其所有子节点。

如果你想测试删除每一个,你可以简单地修改这样的行:

 ctx.DeleteMyEntity(parent); 

或者您想要在树中的任何节点。

显然,我不会得到赏金,但希望我的post能帮助那些寻找适用于任意深度自我引用实体的解决方案的人。

以下是完整的源代码,它是选定答案中Slauma代码的修改版本:

 using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace EFSelfReference { public class MyEntity { public int Id { get; set; } public string Name { get; set; } public int? ParentId { get; set; } public MyEntity Parent { get; set; } public ICollection Children { get; set; } } public class MyContext : DbContext { public DbSet MyEntities { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity() .HasOptional(e => e.Parent) .WithMany(e => e.Children) .HasForeignKey(e => e.ParentId); } public void DeleteMyEntity(MyEntity entity) { var target = MyEntities .Include(x => x.Children) .FirstOrDefault(x => x.Id == entity.Id); RecursiveDelete(target); SaveChanges(); } private void RecursiveDelete(MyEntity parent) { if (parent.Children != null) { var children = MyEntities .Include(x => x.Children) .Where(x => x.ParentId == parent.Id); foreach (var child in children) { RecursiveDelete(child); } } MyEntities.Remove(parent); } } public class TestObjectGraph { public MyEntity RootEntity() { var root = new MyEntity { Name = "Earth", Children = new List { new MyEntity { Name = "Europe", Children = new List { new MyEntity {Name = "Germany"}, new MyEntity { Name = "Ireland", Children = new List { new MyEntity {Name = "Dublin"}, new MyEntity {Name = "Belfast"} } } } }, new MyEntity { Name = "South America", Children = new List { new MyEntity { Name = "Brazil", Children = new List { new MyEntity {Name = "Rio de Janeiro"} } }, new MyEntity {Name = "Chile"}, new MyEntity {Name = "Argentina"} } } } }; return root; } } class Program { static void Main(string[] args) { Database.SetInitializer( new DropCreateDatabaseAlways()); using (var ctx = new MyContext()) { ctx.Database.Initialize(false); ctx.MyEntities.Add(new TestObjectGraph().RootEntity()); ctx.SaveChanges(); } using (var ctx = new MyContext()) { var parent = ctx.MyEntities .Include(e => e.Children) .FirstOrDefault(); var deleteme = parent.Children.First(); ctx.DeleteMyEntity(deleteme); } Console.WriteLine("Completed...."); Console.WriteLine("Press any key to exit"); Console.ReadKey(); } } } 

像以下一样删除父母和孩子对我有用。 子项在父项之前被删除,它是单个数据库往返(一次调用SaveChanges ),当然在一个事务中有三个DELETE语句:

 using (var ctx = new MyContext()) { var parent = ctx.MyEntities.Include(e => e.Children).FirstOrDefault(); foreach (var child in parent.Children.ToList()) ctx.MyEntities.Remove(child); ctx.MyEntities.Remove(parent); ctx.SaveChanges(); } 

(此处使用ToList()是必要的,因为为子项调用Remove也会从父项的Children集合中删除。如果不使用ToList则会抛出foreach循环迭代的集合已被修改的运行时exception。)

为子项和父项调用Remove的顺序无关紧要。 这也有效:

 using (var ctx = new MyContext()) { var parent = ctx.MyEntities.Include(e => e.Children).FirstOrDefault(); var children = parent.Children.ToList(); ctx.MyEntities.Remove(parent); foreach (var child in children) ctx.MyEntities.Remove(child); ctx.SaveChanges(); } 

在两种情况下,EF都以正确的顺序对DELETE语句进行排序。

完整的测试程序(EF 5 / .NET 4.5 / SQL Server):

 using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace EFSelfReference { public class MyEntity { public int Id { get; set; } public string Name { get; set; } public int? ParentId { get; set; } public MyEntity Parent { get; set; } public ICollection Children { get; set; } } public class MyContext : DbContext { public DbSet MyEntities { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity() .HasOptional(e => e.Parent) .WithMany(e => e.Children) .HasForeignKey(e => e.ParentId); } } class Program { static void Main(string[] args) { Database.SetInitializer( new DropCreateDatabaseAlways()); using (var ctx = new MyContext()) { ctx.Database.Initialize(false); var parent = new MyEntity { Name = "Parent", Children = new List() }; parent.Children.Add(new MyEntity { Name = "Child 1" }); parent.Children.Add(new MyEntity { Name = "Child 2" }); ctx.MyEntities.Add(parent); ctx.SaveChanges(); } using (var ctx = new MyContext()) { var parent = ctx.MyEntities.Include(e => e.Children) .FirstOrDefault(); foreach (var child in parent.Children.ToList()) ctx.MyEntities.Remove(child); ctx.MyEntities.Remove(parent); ctx.SaveChanges(); } } } } 

在删除实体之前第一次using包含DB表中当前内容的块之后的屏幕截图:

屏幕1

最后一次SaveChanges之后的SQL profiler的屏幕截图:

屏幕2

即, Parent (Id = 1) 之前删除Child 1 (Id = 2)和Child 2 (Id = 3)。

还有另一种方法,(在做之前考虑回退……)你可以将关系设置为ON DELETE CASCADE,并尝试仅删除父行。