Entity Framework Core (EF Core) は、外部キーを使用するリレーションシップを表します。 外部キーを持つエンティティは、リレーションシップ内の子エンティティまたは依存エンティティです。 このエンティティの外部キー値は、関連するプリンシパル/親エンティティの主キー値 (または代替キー値) と一致する必要があります。
プリンシパル/親エンティティが削除された場合、依存元/子の外部キー値は、 プリンシパル /親の主キーまたは代替キーと一致しなくなります。 これは無効な状態であり、ほとんどのデータベースで参照制約違反が発生します。
この参照制約違反を回避するには、次の 2 つのオプションがあります。
- FK 値を null に設定する
- 依存エンティティまたは子エンティティも削除する
1 つ目のオプションは、外部キー プロパティ (およびマップ先のデータベース列) が null 許容である必要がある、オプションのリレーションシップに対してのみ有効です。
2 番目のオプションは、任意の種類のリレーションシップに対して有効であり、"連鎖削除" と呼ばれます。
ヒント
このドキュメントでは、データベースの更新の観点から、連鎖削除 (および孤立したノードの削除) について説明します。 EF Core の変更の追跡と外部キーとナビゲーションの変更で導入された概念を多用しています。 この資料に取り組む前に、これらの概念を十分に理解しておいてください。
ヒント
GitHub からサンプル コードをダウンロードすることで、このドキュメントのすべてのコードを実行してデバッグできます。
連鎖動作が発生した場合
依存エンティティまたは子エンティティを現在のプリンシパル/親に関連付けることができなくなった場合は、連鎖削除が必要です。 これは、プリンシパル/親が削除された場合や、プリンシパル/親がまだ存在していても依存/子が関連付けられていない場合に発生する可能性があります。
プリンシパル/親の削除
Blog
が依存/子であるPost
との関係のプリンシパル/親であるこの単純なモデルについて考えてみましょう。
Post.BlogId
は外部キー プロパティです。値は、投稿が属するブログの Blog.Id
主キーと一致する必要があります。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
規則により、 Post.BlogId
外部キー プロパティは null 非許容であるため、このリレーションシップは必須として構成されます。 必須のリレーションシップは、既定で連鎖削除を使用するように構成されます。 リレーションシップのモデリングの詳細については、「 リレーションシップ 」を参照してください。
ブログを削除すると、すべての投稿が連鎖的に削除されます。 例えば次が挙げられます。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
SaveChanges では、SQL Server を例として使用して、次の SQL が生成されます。
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
リレーションシップの切断
ブログを削除するのではなく、各投稿とそのブログの関係を断ち切ることができました。 これを行うには、各投稿の参照ナビゲーション Post.Blog
を null に設定します。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
await context.SaveChangesAsync();
また、 Blog.Posts
コレクション ナビゲーションから各投稿を削除することで、リレーションシップを切断することもできます。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
blog.Posts.Clear();
await context.SaveChangesAsync();
どちらの場合も結果は同じです。ブログは削除されませんが、ブログに関連付けられていない投稿は削除されます。
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
プリンシパル/依存に関連付けられていないエンティティの削除は、「オーファン削除」と呼ばれます。
ヒント
連鎖削除と孤立エントリの削除は密接に関連しています。 どちらの場合も、必要なプリンシパル/親とのリレーションシップが切断されると、依存エンティティまたは子エンティティが削除されます。 連鎖削除の場合、プリンシパル/親自体が削除されるため、この切断が発生します。 孤立した子の場合、主体/親エンティティは引き続き存在しますが、依存エンティティまたは子エンティティには関連付けられなくなります。
カスケード動作が発生する場所
カスケード動作は、次の場合に適用できます。
- 現在の追跡システムによって監視されるエンティティ DbContext
- コンテキストに読み込まれていないデータベース内のエンティティ
追跡対象エンティティの連鎖削除
EF Core は常に、追跡対象エンティティに構成されたカスケード動作を適用します。 つまり、上記の例に示すように、アプリケーションが関連するすべての依存/子エンティティを DbContext に読み込む場合、データベースの構成方法に関係なく、カスケード動作が正しく適用されます。
ヒント
追跡対象エンティティにカスケード動作が発生する正確なタイミングは、 ChangeTracker.CascadeDeleteTiming と ChangeTracker.DeleteOrphansTimingを使用して制御できます。 詳細については、「 外部キーとナビゲーションの変更 」を参照してください。
データベースでの連鎖削除
また、多くのデータベース システムでは、データベース内のエンティティが削除されたときにトリガーされる連鎖動作も提供されます。 EF Core では、 EnsureCreated または EF Core の移行を使用してデータベースを作成するときに、EF Core モデルの連鎖削除動作に基づいてこれらの動作を構成します。 たとえば、上記のモデルを使用すると、SQL Server を使用する場合に投稿用に次の表が作成されます。
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
[BlogId] int NOT NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) ON DELETE CASCADE
);
ブログと投稿の間のリレーションシップを定義する外部キー制約は、 ON DELETE CASCADE
で構成されていることに注意してください。
データベースがこのように構成されていることがわかっている場合は、 最初に投稿を読み込まず にブログを削除でき、そのブログに関連するすべての投稿がデータベースによって削除されます。 例えば次が挙げられます。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
投稿に Include
がないため、読み込まれません。 この場合の SaveChanges では、追跡対象の唯一のエンティティであるため、ブログのみが削除されます。
-- Executed DbCommand (6ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
データベース内の外部キー制約が連鎖削除用に構成されていない場合は、例外が発生します。 ただし、この場合、投稿は作成時に ON DELETE CASCADE
で構成されているため、データベースによって削除されます。
注
通常、データベースには孤立データを自動的に消去する方法はありません。 これは、EF Core はナビゲーションと外部キーを使用するリレーションシップを表しますが、データベースには外部キーのみが含まれており、ナビゲーションがないためです。 つまり、通常、DbContext に両側を読み込まずにリレーションシップを切断することはできません。
注
EF Core のメモリ内データベースでは、現在、データベースでの連鎖削除はサポートされていません。
Warnung
エンティティを論理的に削除する場合は、データベースで連鎖削除を構成しないでください。 これにより、エンティティが論理削除されるのではなく、誤って物理的に削除される可能性があります。
データベース カスケードの制限事項
一部のデータベース (特に SQL Server) には、サイクルを形成する連鎖動作に制限があります。 たとえば、次のモデルを考えてみましょう。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
public int OwnerId { get; set; }
public Person Owner { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public int AuthorId { get; set; }
public Person Author { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
public Blog OwnedBlog { get; set; }
}
このモデルには 3 つのリレーションシップがあり、すべて必須であるため、規則によって連鎖削除するように構成されています。
- ブログを削除すると、関連するすべての投稿が連鎖的に削除されます
- 投稿の作成者を削除すると、作成された投稿が連鎖的に削除されます
- ブログの所有者を削除すると、ブログが連鎖的に削除されます
ブログ管理ポリシーがやや厳格だとすれば、これはすべて妥当です。しかし、これらのカスケードを構成した SQL Server データベースを作成しようとすると、以下の例外が発生します。
Microsoft.Data.SqlClient.SqlException (0x80131904): テーブル 'Posts' に FOREIGN KEY 制約 'FK_Posts_Person_AuthorId' を導入すると、サイクルまたは複数のカスケード パスが発生する可能性があります。 ON DELETE NO ACTION、ON UPDATE NO ACTION、を指定するか、他の FOREIGN KEY 制約を変更してください。
この状況を処理するには、次の 2 つの方法があります。
- 連鎖削除しないように 1 つ以上のリレーションシップを変更します。
- これらの連鎖削除を 1 つ以上行わずにデータベースを構成し、EF Core がカスケード動作を実行できるように、すべての依存エンティティが読み込まれるようにします。
この例の最初のアプローチでは、null 許容外部キー プロパティを指定することで、ブログ後のリレーションシップを省略可能にすることができます。
public int? BlogId { get; set; }
省略可能なリレーションシップを使用すると、ブログがなくても投稿が存在できるようになります。つまり、既定では連鎖削除は構成されません。 これは、連鎖アクションのサイクルがなくなったため、SQL Server でエラーなしでデータベースを作成できることを意味します。
代わりに 2 つ目のアプローチを使用すると、ブログ所有者のリレーションシップを必須のままにして連鎖削除用に構成できますが、この構成は、データベースではなく追跡対象エンティティにのみ適用されます。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
ここで、ユーザーと所有するブログの両方を読み込み、そのユーザーを削除するとどうなりますか?
using var context = new BlogsContext();
var owner = await context.People.SingleAsync(e => e.Name == "ajcvickers");
var blog = await context.Blogs.SingleAsync(e => e.Owner == owner);
context.Remove(owner);
await context.SaveChangesAsync();
EF Core は所有者の削除を連鎖して、ブログも削除されるようにします。
-- Executed DbCommand (8ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [People]
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
ただし、所有者が削除されたときにブログが読み込まれていない場合:
using var context = new BlogsContext();
var owner = await context.People.SingleAsync(e => e.Name == "ajcvickers");
context.Remove(owner);
await context.SaveChangesAsync();
その結果、データベース内の外部キー制約に違反したため、例外が発生します。
Microsoft.Data.SqlClient.SqlException: DELETE ステートメントが REFERENCE 制約 "FK_Blogs_People_OwnerId" と競合しています。 データベース "Scratch"、テーブル "dbo.Blogs"、列 'OwnerId' で競合が発生しました。 ステートメントは終了されました。
カスケードされた null 値
オプションのリレーションシップでは、null 許容の外部キー プロパティが、null 許容のデータベース列にマッピングされています。 つまり、現在のプリンシパル/親が削除されたとき、または依存/子から切断された場合に、外部キー値を null に設定できます。
連鎖動作が発生した場合の例をもう一度見てみましょうが、今回は null 許容Post.BlogId
外部キー プロパティで表される省略可能なリレーションシップを使用します。
public int? BlogId { get; set; }
この外部キー プロパティは、関連するブログが削除されると、投稿ごとに null に設定されます。 たとえば、このコードは以前と同じです。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
SaveChanges が呼び出されると、次のデータベースが更新されるようになります。
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (1ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;
同様に、上記のいずれかの例を使用してリレーションシップが切断された場合は、次のようになります。
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
await context.SaveChangesAsync();
または:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
blog.Posts.Clear();
await context.SaveChangesAsync();
その後、SaveChanges が呼び出されると、投稿は null 外部キー値で更新されます。
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
EF Core で 外部キーとナビゲーション の値が変更されたときの管理方法の詳細については、「外部キーとナビゲーションの変更」を参照してください。
注
このようなリレーションシップの修正は、2008 年の最初のバージョン以降の Entity Framework の既定の動作です。 EF Core より前は名前がないため、変更できませんでした。 次のセクションで説明するように、 ClientSetNull
と呼ばれるようになりました。
オプションのリレーションシップのプリンシパル/親が削除された場合に、このような null をカスケードするようにデータベースを構成することもできます。 ただし、これはデータベースで連鎖削除を使用する場合よりもはるかに一般的ではありません。 データベースで連鎖削除と連鎖 null を同時に使用すると、ほとんどの場合、SQL Server を使用するとリレーションシップ サイクルが発生します。 カスケード null の構成の詳細については、次のセクションを参照してください。
カスケード動作の構成
ヒント
ここに来る前に、必ず上記のセクションをお読みください。 上記の資料が理解されていない場合、構成オプションは意味をなさない可能性があります。
連鎖動作は、OnDeleteの OnModelCreating メソッドを使用してリレーションシップごとに構成されます。 例えば次が挙げられます。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
エンティティ型間の リレーションシップ の構成の詳細については、「リレーションシップ」を参照してください。
OnDelete
は、確かに混乱を招く、 DeleteBehavior 列挙型からの値を受け取ります。 この列挙型は、追跡対象エンティティでの EF Core の動作と、EF を使用してスキーマを作成するときのデータベースでの連鎖削除の構成の両方を定義します。
データベース スキーマへの影響
次の表に、EF Core の移行またはOnDelete
によって作成された外部キー制約の各EnsureCreated値の結果を示します。
削除動作 | データベース スキーマへの影響 |
---|---|
伝播 | ON DELETE カスケード削除 |
制限 | 削除時制約 |
アクションなし | データベースの既定値 |
SetNull | ON DELETE SET NULL (削除時にNULLを設定する) |
クライアント設定をNULLに設定 | データベースの既定値 |
ClientCascade | データベースの既定値 |
ClientNoAction | データベースの既定値 |
リレーショナル データベースの ON DELETE NO ACTION
(データベースの既定値) と ON DELETE RESTRICT
の動作は、通常、同じか非常に似ています。
NO ACTION
意味する可能性があるにもかかわらず、これらのオプションの両方によって参照制約が適用されます。 違いがある場合、それはデータベースが制約を確認する時です。 データベース システムの ON DELETE NO ACTION
と ON DELETE RESTRICT
の具体的な違いについては、データベースのドキュメントを参照してください。
SQL Server は ON DELETE RESTRICT
をサポートしていないため、代わりに ON DELETE NO ACTION
が使用されます。
データベースで連鎖動作を引き起こす唯一の値は、 Cascade
と SetNull
です。 その他の値はすべて、変更を連鎖しないようにデータベースを構成します。
SaveChanges の動作への影響
次のセクションの表では、プリンシパル/親が削除されたとき、または依存/子エンティティとのリレーションシップが切断されたときに依存/子エンティティに対して何が起こるかを説明します。 各テーブルには、次のいずれかが含まれます。
- 任意 (null 許容 FK) の関連性と必須 (null 非許容 FK) の関連性
- 依存オブジェクト/子が DbContext によって読み込まれ、追跡され、データベースにのみ存在する場合
読み込まれた依存/子との必須リレーションシップ
削除動作 | プリンシパルまたは親を削除する際 | プリンシパルまたは親からの切り離し |
---|---|---|
伝播 | EF Core によって削除された従属エンティティ | EF Core によって削除された従属エンティティ |
制限 | InvalidOperationException |
InvalidOperationException |
アクションなし | InvalidOperationException |
InvalidOperationException |
SetNull |
SqlException データベースの作成時 |
SqlException データベースの作成時 |
クライアント設定をNULLに設定 | InvalidOperationException |
InvalidOperationException |
ClientCascade | EF Core によって削除された従属エンティティ | EF Core によって削除された従属エンティティ |
ClientNoAction | DbUpdateException |
InvalidOperationException |
注記:
- このような必要なリレーションシップの既定値は
Cascade
です。 - 必要なリレーションシップに連鎖削除以外のものを使用すると、SaveChanges が呼び出されたときに例外が発生します。
- 通常、これは EF Core からの
InvalidOperationException
です。これは、読み込まれた子/依存オブジェクトで無効な状態が検出されるためです。 -
ClientNoAction
により、EF Core は修正依存をデータベースに送信する前にチェックしないように強制するため、この場合、データベースは例外をスローし、SaveChanges によってDbUpdateException
にラップされます。 -
SetNull
は、外部キー列が null 許容ではないため、データベースの作成時に拒否されます。
- 通常、これは EF Core からの
- 依存関係/子が読み込まれると、EF Core によって常に削除され、データベースによって削除されることは決してありません。
必要な扶養家族/子供との関係が読み込まれませんでした
削除動作 | プリンシパルまたは親を削除する際 | プリンシパルまたは親からの切り離し |
---|---|---|
伝播 | データベースにより削除された従属 | なし |
制限 | DbUpdateException |
なし |
アクションなし | DbUpdateException |
なし |
SetNull |
SqlException データベースの作成時 |
なし |
クライアント設定をNULLに設定 | DbUpdateException |
なし |
ClientCascade | DbUpdateException |
なし |
ClientNoAction | DbUpdateException |
なし |
注記:
- 依存関係/子が読み込まれないため、リレーションシップの切断は有効ではありません。
- このような必要なリレーションシップの既定値は
Cascade
です。 - 必要なリレーションシップに連鎖削除以外のものを使用すると、SaveChanges が呼び出されたときに例外が発生します。
- 通常、これは
DbUpdateException
です。依存する子が読み込まれないため、無効な状態はデータベースによってのみ検出できます。 その後、SaveChanges はデータベース例外をDbUpdateException
でラップします。 -
SetNull
は、外部キー列が null 許容ではないため、データベースの作成時に拒否されます。
- 通常、これは
オプションの扶養家族/子供との関係が読み込まれた
削除動作 | プリンシパルまたは親を削除する際 | プリンシパルまたは親からの切り離し |
---|---|---|
伝播 | EF Core によって削除された従属エンティティ | EF Core によって削除された従属エンティティ |
制限 | EF Core によって依存 FK が null に設定される | EF Core によって依存 FK が null に設定される |
アクションなし | EF Core によって依存 FK が null に設定される | EF Core によって依存 FK が null に設定される |
SetNull | EF Core によって依存 FK が null に設定される | EF Core によって依存 FK が null に設定される |
クライアント設定をNULLに設定 | EF Core によって依存 FK が null に設定される | EF Core によって依存 FK が null に設定される |
ClientCascade | EF Core によって削除された従属エンティティ | EF Core によって削除された従属エンティティ |
ClientNoAction | DbUpdateException |
EF Core によって依存 FK が null に設定される |
注記:
- このような省略可能なリレーションシップの既定値は
ClientSetNull
です。 -
Cascade
またはClientCascade
が構成されていない限り、依存/子は削除されません。 - その他のすべての値により、依存する FK が EF Core によって null に設定されます。...
- ...ただし
ClientNoAction
はプリンシパル/親が削除されたときに、依存/子の外部キーを変更しないように EF Core に指示するものです。 したがって、データベースは例外をスローします。例外は SaveChanges によってDbUpdateException
としてラップされます。
- ...ただし
依存関係/子どもとのオプションの関係が読み込まれていません
削除動作 | プリンシパルまたは親を削除する際 | プリンシパルまたは親からの切り離し |
---|---|---|
伝播 | データベースにより削除された従属 | なし |
制限 | DbUpdateException |
なし |
アクションなし | DbUpdateException |
なし |
SetNull | データベースによって依存 FK が null に設定される | なし |
クライアント設定をNULLに設定 | DbUpdateException |
なし |
ClientCascade | DbUpdateException |
なし |
ClientNoAction | DbUpdateException |
なし |
注記:
- 依存関係/子が読み込まれないため、リレーションシップの切断は有効ではありません。
- このような省略可能なリレーションシップの既定値は
ClientSetNull
です。 - データベースが削除または null を連鎖するように構成されていない限り、データベースの例外を回避するために、依存/子を読み込む必要があります。
.NET