16

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

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

当我尝试删除子项及其父项时,EF 向数据库发出的 DELETE 命令与我期望的顺序不同 - 它首先尝试删除父记录。

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

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

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

4

3 回答 3

20

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

例如,考虑以下结构:

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

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

我提交以下代码作为替代代码(修改由 Slauma 提供的答案,顺便说一句做得很好)。

在 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<MyEntity>
                    {
                        new MyEntity
                        {
                            Name = "Europe",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity {Name = "Germany"},
                                    new MyEntity
                                    {
                                        Name = "Ireland",
                                        Children =
                                            new List<MyEntity>
                                            {
                                                new MyEntity {Name = "Dublin"},
                                                new MyEntity {Name = "Belfast"}
                                            }
                                    }
                                }
                        },
                        new MyEntity
                        {
                            Name = "South America",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity
                                    {
                                        Name = "Brazil",
                                        Children = new List<MyEntity>
                                        {
                                            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                 
 *               
 */

其中欧洲及其所有子项都被删除。

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

如果你想测试删除everyting,你可以简单地修改如下行:

ctx.DeleteMyEntity(parent);

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

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

这是完整的源代码,它是所选答案中 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<MyEntity> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<MyEntity>()
                .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<MyEntity>
                    {
                        new MyEntity
                        {
                            Name = "Europe",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity {Name = "Germany"},
                                    new MyEntity
                                    {
                                        Name = "Ireland",
                                        Children =
                                            new List<MyEntity>
                                            {
                                                new MyEntity {Name = "Dublin"},
                                                new MyEntity {Name = "Belfast"}
                                            }
                                    }
                                }
                        },
                        new MyEntity
                        {
                            Name = "South America",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity
                                    {
                                        Name = "Brazil",
                                        Children = new List<MyEntity>
                                        {
                                            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<MyContext>(
               new DropCreateDatabaseAlways<MyContext>());
            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();
        }
    }
}
于 2014-08-06T19:44:55.893 回答
13

像下面这样删除父母和孩子对我有用。子级在父级之前被删除,它是一个单一的数据库往返(一次调用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中删除。如果不使用,则会抛出运行时异常,表明循环正在迭代的集合已被修改。)ChildrenToListforeach

为孩子和父母调用的顺序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<MyEntity> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<MyEntity>()
                .HasOptional(e => e.Parent)
                .WithMany(e => e.Children)
                .HasForeignKey(e => e.ParentId);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer<MyContext>(
                new DropCreateDatabaseAlways<MyContext>());
            using (var ctx = new MyContext())
            {
                ctx.Database.Initialize(false);

                var parent = new MyEntity { Name = "Parent",
                    Children = new List<MyEntity>() };

                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

最后一个之后的 SQL 分析器的屏幕截图SaveChanges

屏幕 2

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

于 2013-07-16T17:23:12.437 回答
0

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

于 2013-07-17T21:05:59.473 回答