web-dev-qa-db-ja.com

DELETEステートメントがEntityFrameworkのSAMETABLEREFERENCE制約と競合していました

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に伝える何らかの明示的な方法を見逃していますか?たぶん、IDの降順で削除するようにEFに指示する方法はありますか?わからない..考え?

15
Kon

答えは1年前だと思いますが、不完全だと思います。私の考えでは、自己参照テーブルは任意の深さを表すために使用されます。

たとえば、次の構造について考えてみます。

/*  
 *  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                 
 *               
 */

ヨーロッパとそのすべての子が削除されます。

上記では、ルートノードの最初の子を指定しています。これは、コードを使用して、階層内のどこからでもノードとそのすべての子を再帰的に削除できることを示しています。

すべての削除をテストする場合は、次のように行を変更するだけです。

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();
        }
    }
}
18
geekzster

次のように親子を削除すると、うまくいきます。子は削除されますbefore親であり、これは単一のデータベースラウンドトリップ(SaveChangesへの1回の呼び出し)であり、もちろん1つのトランザクションで3つの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ループが反復しているコレクションが変更されました。)

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();
            }
        }
    }
}
_

エンティティが削除される前の、DBテーブルの現在のコンテンツを含む最初のusingブロックの後のスクリーンショット:

screen 1

最後のSaveChangesの後のSQLプロファイラーのスクリーンショット:

screen 2

つまり_Child 1_(Id = 2)および_Child 2_(Id = 3)が削除されますParent(Id = 1)。

12
Slauma

別の方法があります(実行する前に欠点について考えてください...)関係をON DELETE CASCADEに設定し、親行のみを削除してみてください。

0
Aram