ParentIdがID(PK)へのFKである自己参照を持つテーブルがあります。
EF(コードファースト)を使用して、次のように関係を設定しました。
this.HasOptional(t => t.ParentValue)
.WithMany(t => t.ChildValues)
.HasForeignKey(t => t.ParentId);
子とその親を削除しようとすると、データベースに対してEFが発行するDELETEコマンドは、期待した順序ではありません。最初に親レコードを削除しようとします。
ここにはいくつかのオプションがあることに気づきました(どちらも好きではありません)。
だから問題は..親レコードの前に子の削除を強制する方法はありますか?おそらく私は、親の前にこれらの子供たちの世話をする必要があることをEFに伝える何らかの明示的な方法を見逃していますか?たぶん、IDの降順で削除するようにEFに指示する方法はありますか?わからない..考え?
答えは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();
}
}
}
次のように親子を削除すると、うまくいきます。子は削除されます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
ブロックの後のスクリーンショット:
最後のSaveChanges
の後のSQLプロファイラーのスクリーンショット:
つまり_Child 1
_(Id = 2)および_Child 2
_(Id = 3)が削除されます前Parent
(Id = 1)。
別の方法があります(実行する前に欠点について考えてください...)関係をON DELETE CASCADEに設定し、親行のみを削除してみてください。