次の方法で共有


外部キーとナビゲーションの変更

外部キーとナビゲーションの概要

Entity Framework Core (EF Core) モデルのリレーションシップは、外部キー (FK) を使用して表されます。 FK は、リレーションシップ内の依存エンティティまたは子エンティティの 1 つ以上のプロパティで構成されます。 依存/子の外部キー プロパティの値がプリンシパル/親の代替または主キー (PK) プロパティの値と一致する場合、この依存/子エンティティは特定のプリンシパル/親エンティティに関連付けられます。

外部キーは、データベース内のリレーションシップを格納および操作するための優れた方法ですが、アプリケーション コードで複数の関連エンティティを操作する場合はあまりわかりやすいものではありません。 そのため、ほとんどの EF Core モデルでは、FK 表現に "ナビゲーション" も重ねて表示されます。 ナビゲーションは、外部キー値を主キー値または代替キー値と照合することによって検出された関連付けを反映するエンティティ インスタンス間の C#/.NET 参照を形成します。

ナビゲーションは、リレーションシップの両側で使用することも、一方の側でのみ使用することも、まったく使用せずにFKプロパティのみを残すこともできます。 FK プロパティは 、シャドウ プロパティにすることで非表示にすることができます。 モデリング リレーションシップ の詳細については、「リレーションシップ」を参照してください。

ヒント

このドキュメントでは、エンティティの状態と EF Core の変更追跡の基本が理解されていることを前提としています。 これらのトピックの詳細については、 EF Core の変更の追跡 を参照してください。

ヒント

GitHub からサンプル コードをダウンロードすることで、このドキュメントのすべてのコードを実行してデバッグできます。

モデルの例

次のモデルには、それらの間のリレーションシップを持つ 4 つのエンティティ型が含まれています。 コード内のコメントは、どのプロパティが外部キー、主キー、ナビゲーションであるかを示しています。

public class Blog
{
    public int Id { get; set; } // Primary key
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

このモデルの 3 つのリレーションシップは次のとおりです。

  • 各ブログには、多くの投稿 (1 対多) を持つことができます。
    • Blog はプリンシパル/親です。
    • Post は依存/子です。 これには FK プロパティ Post.BlogIdが含まれています。この値は、関連するブログの Blog.Id PK 値と一致する必要があります。
    • Post.Blog は、投稿から関連するブログへの参照ナビゲーションです。 Post.Blog は、 Blog.Postsの逆ナビゲーションです。
    • Blog.Posts は、ブログから関連するすべての投稿へのコレクション ナビゲーションです。 Blog.Posts は、 Post.Blogの逆ナビゲーションです。
  • 各ブログには、1 つのアセット(1 対 1)を含めることができます。
    • Blog はプリンシパル/親です。
    • BlogAssets は依存/子です。 これには FK プロパティ BlogAssets.BlogIdが含まれています。この値は、関連するブログの Blog.Id PK 値と一致する必要があります。
    • BlogAssets.Blog は、資産から関連するブログへの参照ナビゲーションです。 BlogAssets.Blog は、 Blog.Assetsの逆ナビゲーションです。
    • Blog.Assets は、ブログから関連する資産への参照ナビゲーションです。 Blog.Assets は、 BlogAssets.Blogの逆ナビゲーションです。
  • 各投稿には多くのタグを付けることができ、各タグには多くの投稿 (多対多) を含めることができます。
    • 多対多リレーションシップは、2つの一対多リレーションシップを土台にしたさらなるレイヤーです。 多対多リレーションシップについては、このドキュメントの後半で説明します。
    • Post.Tags は、投稿から関連するすべてのタグへのコレクション ナビゲーションです。 Post.Tags は、 Tag.Postsの逆ナビゲーションです。
    • Tag.Posts は、タグから関連付けられているすべての投稿へのコレクション ナビゲーションです。 Tag.Posts は、 Post.Tagsの逆ナビゲーションです。

リレーションシップをモデル化および構成する方法の詳細については、「リレーションシップ」を参照してください。

リレーションシップの修正

EF Core では、ナビゲーションと外部キー値が互いに整合性を保つように維持されます。 つまり、外部キー値が異なるプリンシパル/親エンティティを参照するように変更された場合、ナビゲーションはこの変更を反映するように更新されます。 同様に、ナビゲーションが変更されると、関連するエンティティの外部キー値がこの変更を反映するように更新されます。 これは "リレーションシップの修正" と呼ばれます。

クエリによる修正

修正は、データベースからエンティティが照会されるときに最初に発生します。 データベースには外部キー値しかないため、EF Core はデータベースからエンティティ インスタンスを作成するときに、外部キー値を使用して参照ナビゲーションを設定し、必要に応じてエンティティをコレクション ナビゲーションに追加します。 たとえば、ブログとその関連する投稿と資産のクエリを考えてみましょう。

using var context = new BlogsContext();

var blogs = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

各ブログについて、EF Core は最初に Blog インスタンスを作成します。 次に、各投稿がデータベースから読み込まれると、 Post.Blog 参照ナビゲーションが関連付けられているブログを指すように設定されます。 同様に、投稿は Blog.Posts コレクション ナビゲーションに追加されます。 この場合、両方のナビゲーションが参照である点を除き、 BlogAssetsでも同じことが起こります。 Blog.Assets ナビゲーションは資産インスタンスを指し、BlogAsserts.Blog ナビゲーションはブログ インスタンスを指すよう設定されます。

このクエリの後の 変更トラッカーデバッグ ビュー を見ると、2 つのブログが表示されます。それぞれに 1 つの資産と 2 つの投稿が追跡されています。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

デバッグ ビューには、キー値とナビゲーションの両方が表示されます。 ナビゲーションは、関連エンティティの主キー値を使用して表示されます。 たとえば、上の出力の Posts: [{Id: 1}, {Id: 2}] は、 Blog.Posts コレクション ナビゲーションに、それぞれ主キー 1 と 2 を持つ 2 つの関連する投稿が含まれていることを示しています。 同様に、最初のブログに関連付けられている投稿ごとに、 Blog: {Id: 1} 行は、 Post.Blog ナビゲーションが主キー 1 を持つブログを参照していることを示します。

ローカルで追跡されるエンティティの調整

また、リレーションシップの修正は、追跡クエリから返されるエンティティと、DbContext によって既に追跡されているエンティティの間でも発生します。 たとえば、ブログ、投稿、資産に対して 3 つの個別のクエリを実行することを検討してください。

using var context = new BlogsContext();

var blogs = await context.Blogs.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = await context.Assets.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = await context.Posts.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

デバッグ ビューをもう一度見ると、最初のクエリの後、2 つのブログのみが追跡されます。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Blog.Assets参照ナビゲーションは null であり、関連付けられたエンティティが現在コンテキストによって追跡されていないため、Blog.Posts コレクションのナビゲーションは空です。

2 番目のクエリの後、 Blogs.Assets 参照ナビゲーションは、新しく追跡された BlogAsset インスタンスを指すまで修正されています。 同様に、 BlogAssets.Blog 参照ナビゲーションは、既に追跡されている適切な Blog インスタンスを指すよう設定されます。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

最後に、3 番目のクエリの後に、 Blog.Posts コレクションナビゲーションに関連するすべての投稿が含まれるようになり、 Post.Blog 参照は適切な Blog インスタンスを指します。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

これは、元の単一クエリで達成されたのと同じ終了状態です。これは、複数の異なるクエリから取得された場合でも、エンティティが追跡されたときに EF Core によってナビゲーションが修正されたためです。

修正によって、データベースから返されるデータが増えることはありません。 クエリによって既に返されたエンティティ、または DbContext によって既に追跡されているエンティティのみが接続されます。 エンティティをシリアル化するときの重複の処理については、 EF Core での ID 解決 に関する説明を参照してください。

ナビゲーションを使用してリレーションシップを変更する

2 つのエンティティ間のリレーションシップを変更する最も簡単な方法は、ナビゲーションを操作しながら、EF Core を離れて逆ナビゲーションと FK 値を適切に修正することです。 これは、次の方法で実行できます。

  • コレクション ナビゲーションに対するエンティティの追加または削除。
  • 別のエンティティを指す参照ナビゲーションを変更するか、null に設定します。

コレクション ナビゲーションの追加または削除

たとえば、Visual Studio ブログから .NET ブログに投稿の 1 つを移動してみましょう。 これには、まずブログと投稿を読み込み、次に、あるブログのナビゲーション コレクションから他のブログのナビゲーション コレクションに投稿を移動する必要があります。

using var context = new BlogsContext();

var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == "Visual Studio Blog");

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

ヒント

デバッグ ビューにアクセスしてもChangeTracker.DetectChanges()されないため、ここでを呼び出す必要があります。

これは、上記のコードを実行した後に出力されるデバッグ ビューです。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

.NET ブログの Blog.Posts ナビゲーションに、3 つの投稿 (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]) が追加されました。 同様に、Visual Studio ブログの Blog.Posts ナビゲーションには 1 つの投稿 (Posts: [{Id: 4}]) しかありません。 これは、コードによってこれらのコレクションが明示的に変更されるためです。

さらに興味深いことに、コードは Post.Blog ナビゲーションを明示的に変更しなかったとしても、Visual Studio ブログ (Blog: {Id: 1}) を指すよう修正されています。 また、.NET ブログの主キー値と一致するように、 Post.BlogId 外部キーの値が更新されました。 この FK 値への変更は、SaveChanges が呼び出されたときにデータベースに永続化されます。

-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

参照ナビゲーションの変更

前の例では、各ブログの投稿のコレクション ナビゲーションを操作して、あるブログから別のブログに投稿を移動しました。 新しいブログを指すように Post.Blog 参照ナビゲーションを変更することで、同じことを実現できます。 例えば次が挙げられます。

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

この変更後のデバッグ ビューは、前の例と まったく同じです 。 これは、EF Core で参照ナビゲーションの変更が検出され、コレクション ナビゲーションと FK 値が一致するように修正されたためです。

外部キー値を使用したリレーションシップの変更

前のセクションでは、リレーションシップはナビゲーションによって操作され、外部キーの値は自動的に更新されます。 これは、EF Core でリレーションシップを操作するための推奨される方法です。 ただし、FK 値を直接操作することもできます。 たとえば、 Post.BlogId 外部キーの値を変更することで、ブログ間で投稿を移動できます。

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

前の例に示すように、これは参照ナビゲーションの変更とよく似ています。

この変更後のデバッグ ビューは、前の 2 つの例の場合と まったく同じです 。 これは、EF Core が FK 値の変更を検出し、一致するように参照ナビゲーションとコレクション ナビゲーションの両方を修正したためです。

ヒント

リレーションシップが変更されるたびに、すべてのナビゲーションと FK 値を操作するコードを記述しないでください。 このようなコードはより複雑であり、すべてのケースで外部キーとナビゲーションに対して一貫した変更を行う必要があります。 可能であれば、単一のナビゲーション、または両方のナビゲーションを操作するだけです。 必要に応じて、FK 値を操作するだけです。 ナビゲーションと FK 値の両方を操作しないでください。

追加または削除されたエンティティの修正

コレクション ナビゲーションへの追加

EF Core は、新しい依存エンティティまたは子エンティティがコレクション ナビゲーションに追加されたことを 検出 すると、次のアクションを実行します。

  • エンティティが追跡されていない場合は、追跡されます。 (エンティティは通常、 Added 状態になります。ただし、生成されたキーを使用するようにエンティティ型が構成されていて、主キーの値が設定されている場合、エンティティは Unchanged 状態で追跡されます)。
  • エンティティが別のプリンシパル/親に関連付けられている場合、そのリレーションシップは切断されます。
  • エンティティは、コレクション ナビゲーションを所有するプリンシパル/親に関連付けられます。
  • ナビゲーションと外部キーの値は、関連するすべてのエンティティに対して固定されます。

これに基づいて、あるブログから別のブログに投稿を移動するために、新しいブログに追加する前に、実際に古いコレクションナビゲーションから削除する必要がないことがわかります。 そのため、上記の例のコードは次から変更できます。

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

宛先:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

EF Core は、投稿が新しいブログに追加されたことを確認し、最初のブログのコレクションから自動的に削除します。

コレクション ナビゲーションからの削除

プリンシパル/親のコレクション ナビゲーションから依存/子エンティティを削除すると、そのプリンシパル/親との関係が切断されます。 次に何が起こるかは、リレーションシップが省略可能か必須かによって異なります。

省略可能なリレーションシップ

省略可能なリレーションシップの既定では、外部キーの値は null に設定されます。 これは、依存/子 がプリンシパル/ 親に関連付けられていないことを意味します。 たとえば、ブログと投稿を読み込み、 Blog.Posts コレクション ナビゲーションから投稿の 1 つを削除します。

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

この変更後の 変更追跡デバッグ ビュー を見ると、次のことが示されます。

  • Post.BlogId FK が null (BlogId: <null> FK Modified Originally 1) に設定されている
  • Post.Blog参照ナビゲーションが null (Blog: <null>) に設定されました
  • 投稿はコレクション ナビゲーションBlog.Postsから削除されました(Posts: [{Id: 1}])
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

投稿がとしてマークDeletedに注意してください。 SaveChanges が呼び出されたときにデータベースの FK 値が null に設定されるように、 Modified としてマークされます。

必要なリレーションシップ

必要なリレーションシップに対して FK 値を null に設定することはできません (通常は使用できません)。 したがって、必要なリレーションシップを切断すると、参照制約違反を回避するために、依存/子エンティティを新しいプリンシパル/親に再親するか、SaveChanges が呼び出されたときにデータベースから削除する必要があります。 これは「孤児を削除する」と呼ばれ、EF Core の必須リレーションシップにおけるデフォルトの動作です。

たとえば、ブログと投稿の関係を必要に応じて変更し、前の例と同じコードを実行してみましょう。

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

この変更後のデバッグ ビューを見ると、次のことが示されます。

  • この投稿は、SaveChanges が呼び出されたときにデータベースから削除されるように、 Deleted としてマークされています。
  • Post.Blog参照ナビゲーションが null (Blog: <null>) に設定されています。
  • 投稿はBlog.Postsコレクション ナビゲーション (Posts: [{Id: 1}])から削除されました。
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

必要なリレーションシップでは null に設定できないため、 Post.BlogId は変更されません。

SaveChanges を呼び出すと、孤立した投稿が削除されます。

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

孤児化したタイミングと再親化を削除する

既定では、関係の変更がDeletedされるとすぐに、孤児としてがマークされます。 ただし、このプロセスは、SaveChanges が実際に呼び出されるまで遅延する可能性があります。 これは、あるプリンシパル/親から削除されたが、SaveChanges が呼び出される前に新しいプリンシパル/親に再び関連付けられるエンティティが孤立することを回避するのに役立ちます。 ChangeTracker.DeleteOrphansTiming は、このタイミングを設定するために使用されます。 例えば次が挙げられます。

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

最初のコレクションから投稿を削除した後、オブジェクトは前の例のように Deleted としてマークされません。 代わりに、EF Core では、これが 必要なリレーションシップであっても、リレーションシップが切断されたことを追跡しています。 (FK 値は、型が null 許容ではないため、実際には null にすることはできませんが、EF Core によって null と見なされます。これは "概念 null" と呼ばれます)。

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

この時点で SaveChanges を呼び出すと、孤立した投稿が削除されます。 ただし、上記の例のように、SaveChanges が呼び出される前に投稿が新しいブログに関連付けられている場合は、その新しいブログに適切に修正され、孤立していると見なされなくなります。

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

この時点で呼び出された SaveChanges は、削除するのではなく、データベース内の投稿を更新します。

孤立した子の自動削除を無効にすることもできます。 孤立が追跡されている間に SaveChanges が呼び出されると、例外が発生します。 たとえば、次のコードを使用します。

var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

await context.SaveChangesAsync(); // Throws

次の例外がスローされます。

System.InvalidOperationException: エンティティ 'Blog' と 'Post' とキー値 '{BlogId: 1}' の関連付けが切断されましたが、リレーションシップは必須としてマークされているか、外部キーが null 許容ではないため暗黙的に必要です。 必要なリレーションシップが切断されたときに依存/子エンティティを削除する必要がある場合は、連鎖削除を使用するようにリレーションシップを構成します。

ChangeTracker.CascadeChanges()を呼び出すことで、孤立した子の削除と連鎖削除をいつでも強制できます。 これを削除オーファンのタイミングをNeverに設定すると、EF Coreに明示的に指示されない限り、オーファンが削除されることはありません。

参照ナビゲーションの変更

一対多リレーションシップの参照ナビゲーションを変更すると、リレーションシップのもう一方の端でコレクション ナビゲーションを変更する場合と同じ効果があります。 依存/子の参照ナビゲーションを null に設定することは、プリンシパル/親のコレクション ナビゲーションからエンティティを削除することと同じです。 リレーションシップが必要な場合にエンティティを孤立させるなど、前のセクションで説明したように、すべての修正とデータベースの変更が行われます。

オプションの 1 対 1 のリレーションシップ

1 対 1 のリレーションシップの場合、参照ナビゲーションを変更すると、以前のリレーションシップが切断されます。 省略可能なリレーションシップの場合、以前に関連した依存/子の FK 値が null に設定されることを意味します。 例えば次が挙げられます。

using var context = new BlogsContext();

var dotNetBlog = await context.Blogs.Include(e => e.Assets).SingleAsync(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

SaveChanges を呼び出す前のデバッグ ビューは、新しい資産が既存の資産を置き換えたことを示しています。このアセットは、 Modified として null BlogAssets.BlogId FK 値でマークされるようになりました。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

これにより、SaveChanges が呼び出されたときに更新と挿入が行われます。

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

必須の 1 対 1 のリレーションシップ

前の例と同じコードを実行しますが、今回は必要な1対1のリレーションシップがあるため、以前に関連付けられたBlogAssetsDeletedとしてマークされます。これは、新しいBlogAssetsがその場所を取ることで孤立状態(オーファン)になるためです。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

これにより、SaveChanges が呼び出されたときに削除と挿入が行われます。

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

孤立したオブジェクトを削除済みとしてマークするタイミングは、コレクション ナビゲーションに表示されるのと同じ方法で変更でき、同じ効果があります。

エンティティの削除

省略可能なリレーションシップ

エンティティが Deletedとしてマークされている場合 (たとえば、 DbContext.Removeを呼び出すことによって)、削除されたエンティティへの参照は他のエンティティのナビゲーションから削除されます。 オプションのリレーションシップの場合、依存エンティティの FK 値は null に設定されます。

たとえば、Visual Studio ブログを Deletedとしてマークしましょう。

using var context = new BlogsContext();

var vsBlog = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .SingleAsync(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

SaveChanges を呼び出す前に 変更トラッカーのデバッグ ビュー を見ると、次の情報が表示されます。

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

次のことに注意してください。

  • ブログは Deletedとしてマークされています。
  • 削除されたブログに関連する資産には、null FK 値 (BlogId: <null> FK Modified Originally 2) と null 参照ナビゲーション (Blog: <null>) があります。
  • 削除されたブログに関連する各投稿には、null FK 値 (BlogId: <null> FK Modified Originally 2) と null 参照ナビゲーション (Blog: <null>) があります

必要なリレーションシップ

必須リレーションシップの修正動作は省略可能なリレーションシップの場合と同じですが、依存/子エンティティはプリンシパル/親なしでは存在できず、参照制約の例外を回避するために SaveChanges が呼び出されたときにデータベースから削除する必要があるため、 Deleted としてマークされます。 これは "連鎖削除" と呼ばれ、必要なリレーションシップに対する EF Core の既定の動作です。 たとえば、前の例と同じコードを実行し、必要なリレーションシップを使用すると、SaveChanges が呼び出される前に次のデバッグ ビューが表示されます。

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

予想どおり、依存者/子は Deletedとしてマークされるようになりました。 ただし、削除されたエンティティのナビゲーションは変更 されていないこと に注意してください。 これは奇妙に思えるかもしれませんが、すべてのナビゲーションをクリアすることで、削除されたエンティティのグラフを完全に細分化することを回避できます。 つまり、ブログ、資産、投稿は、削除された後もエンティティのグラフを形成します。 これにより、グラフが細断された EF6 の場合よりもはるかに簡単にエンティティのグラフを削除できます。

カスケード削除のタイミングとリペアレント

既定では、親/プリンシパルが Deletedとしてマークされるとすぐに連鎖削除が行われます。 これは、前に説明したように、孤児を削除する場合と同じです。 孤児を削除する場合と同様に、このプロセスは SaveChanges が呼び出されるまで遅延させたり、ChangeTracker.CascadeDeleteTiming を適切に設定することで完全に無効にしたりすることができます。 プリンシパル/親を削除した後に子供/扶養家族を再親化するなど、孤立した子を削除するのに役立つのと同様に、これも同様に役立ちます。

連鎖削除および孤立した項目の削除は、ChangeTracker.CascadeChanges()を呼び出すことでいつでも強制できます。 これをカスケード削除のタイミングを Never に設定すると、EF Core に明示的に指示されない限り、連鎖削除は行われなくなります。

ヒント

連鎖削除と孤立エントリの削除は密接に関連しています。 どちらの場合も、必要なプリンシパル/親とのリレーションシップが切断されると、依存エンティティまたは子エンティティが削除されます。 連鎖削除の場合、プリンシパル/親自体が削除されるため、この切断が発生します。 孤立した子の場合、主体/親エンティティは引き続き存在しますが、依存エンティティまたは子エンティティには関連付けられなくなります。

多対多のリレーションシップ

EF Core の多対多リレーションシップは、結合エンティティを使用して実装されます。 多対多リレーションシップを構成する各側は、一対多リレーションシップを持つこの結合エンティティに関連しています。 この結合エンティティは、明示的に定義およびマップすることも、暗黙的に作成して非表示にすることもできます。 どちらの場合も、基になる動作は同じです。 まず、この基になる動作を見て、多対多リレーションシップの追跡のしくみを理解します。

多対多リレーションシップのしくみ

明示的に定義された結合エンティティ型を使用して、投稿とタグの間に多対多のリレーションシップを作成するこの EF Core モデルを考えてみましょう。

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 IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

PostTag結合エンティティ型に 2 つの外部キー プロパティが含まれていることに注意してください。 このモデルでは、タグに関連する投稿の場合、 PostTag.PostId 外部キー値が Post.Id 主キー値と一致し、 PostTag.TagId 外部キー値が Tag.Id 主キー値と一致する PostTag 結合エンティティが必要です。 例えば次が挙げられます。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

このコードを実行した後の 変更トラッカーのデバッグ ビュー を見ると、投稿とタグが新しい PostTag 結合エンティティによって関連していることを示しています。

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

Postの参照ナビゲーションと同様に、TagPostTagのコレクション ナビゲーションが修正されていることに注意してください。 これらのリレーションシップは、前のすべての例と同様に、FK 値ではなくナビゲーションによって操作できます。 たとえば、上記のコードは、結合エンティティに参照ナビゲーションを設定することで、リレーションシップを追加するように変更できます。

context.Add(new PostTag { Post = post, Tag = tag });

これにより、前の例とまったく同じようにFK(外部キー)およびナビゲーションの変更が行われます。

ナビゲーションをスキップする

結合テーブルを手動で操作するのは面倒な場合があります。 多対多リレーションシップは、結合エンティティを "スキップ" する特別なコレクション ナビゲーションを使用して直接操作できます。 たとえば、上記のモデルに 2 つのスキップ ナビゲーションを追加できます。1 つは投稿からタグ、もう 1 つはタグから投稿へ:

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 IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

この多対多リレーションシップでは、スキップ ナビゲーションと通常のナビゲーションがすべて同じ多対多リレーションシップに使用されるように、次の構成が必要です。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

多対多 リレーションシップ のマッピングの詳細については、Relationships を参照してください。

ナビゲーションをスキップすると、通常のコレクション ナビゲーションのように見え、動作します。 ただし、外部キー値を操作する方法は異なります。 投稿をタグに関連付けますが、今回はスキップ ナビゲーションを使用します。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

このコードでは結合エンティティが使用されない点に注意してください。 代わりに、一対多リレーションシップの場合と同じ方法でエンティティをナビゲーション コレクションに追加するだけです。 結果のデバッグ ビューは、基本的に以前と同じです。

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

現在関連付けられているタグと投稿のPK値に設定されているFK値を用いて、PostTag結合エンティティのインスタンスが自動的に作成されたことに留意してください。 通常の参照ナビゲーションとコレクション ナビゲーションはすべて、これらの FK 値に一致するように修正されています。 また、このモデルにはスキップ ナビゲーションが含まれるため、これらは修正されています。 具体的には、 Post.Tags スキップ ナビゲーションにタグを追加した場合でも、このリレーションシップのもう一方の側の Tag.Posts 逆スキップ ナビゲーションも、関連する投稿を含むよう修正されています。

スキップ ナビゲーションが上に階層化されている場合でも、基になる多対多リレーションシップを直接操作できることは注目に値します。 たとえば、スキップ ナビゲーションを導入する前と同様に、タグと Post を関連付けできます。

context.Add(new PostTag { Post = post, Tag = tag });

または、FK 値を使用します。

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

これにより、スキップ ナビゲーションが正しく修正され、前の例と同じデバッグ ビュー出力が生成されます。

ナビゲーションのみをスキップする

前のセクションでは、基になる 2 つの一対多リレーションシップを完全 に定義するだけでなく 、スキップ ナビゲーションを追加しました。 これは FK 値の動作を示すために役立ちますが、多くの場合は不要です。 代わりに、 スキップ ナビゲーションのみを使用して多対多リレーションシップを定義できます。 これは、このドキュメントの最上部にあるモデルで多対多リレーションシップを定義する方法です。 このモデルを使用すると、ナビゲーションをスキップする Tag.Posts に投稿を追加することで、投稿とタグを再び関連付けることができます (または、 Post.Tags スキップ ナビゲーションにタグを追加することもできます)。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

この変更を行った後のデバッグ ビューを見ると、EF Core が結合エンティティを表す Dictionary<string, object> のインスタンスを作成したことが明らかになります。 この結合エンティティには、関連付けられている投稿とタグの PK 値と一致するように設定されている PostsIdTagsId の両方の外部キー プロパティが含まれています。

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

暗黙的な結合エンティティと、エンティティ型の使用の詳細については、「Dictionary<string, object>を参照してください。

Von Bedeutung

規則によってエンティティ型を結合するために使用される CLR 型は、パフォーマンスを向上させるために将来のリリースで変更される可能性があります。 明示的に構成されていない限り、 Dictionary<string, object> される結合の種類に依存しないでください。

ペイロードを使用してエンティティを結合する

これまで、すべての例では、多対多リレーションシップに必要な 2 つの外部キー プロパティのみを含む結合エンティティ型 (明示的か暗黙的かに関係なく) を使用してきました。 これらの FK 値は、リレーションシップを操作するときにアプリケーションで明示的に設定する必要もありません。これらの値は、関連エンティティの主キー プロパティから取得されるためです。 これにより、EF Core は、データを失うことなく結合エンティティのインスタンスを作成できます。

生成された値を持つペイロード

EF Core では、結合エンティティ型へのプロパティの追加がサポートされています。 これは、結合エンティティに "ペイロード" を与えることと呼ばれます。 たとえば、TaggedOnプロパティをPostTag結合エンティティに追加してみましょう。

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

EF Core が結合エンティティ インスタンスを作成する場合、このペイロード プロパティは設定されません。 これに対処する最も一般的な方法は、自動的に生成された値でペイロード プロパティを使用することです。 たとえば、 TaggedOn プロパティは、各新しいエンティティが挿入されたときに、ストアで生成されたタイムスタンプを使用するように構成できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

投稿は、以前と同じ方法でタグ付けできるようになりました。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

SaveChanges を呼び出した後の 変更トラッカーデバッグ ビュー を見ると、ペイロード プロパティが適切に設定されていることを示しています。

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

ペイロード値を明示的に設定する

前の例に従って、自動生成された値を使用しないペイロード プロパティを追加しましょう。

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

投稿に以前と同じ方法でタグを付けることができ、結合エンティティは自動的に作成されます。 このエンティティには、「 追跡対象エンティティへのアクセス」で説明されているメカニズムのいずれかを使用してアクセスできます。 たとえば、次のコードでは、 DbSet<TEntity>.Find を使用して結合エンティティ インスタンスにアクセスします。

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = await context.Set<PostTag>().FindAsync(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

結合エンティティが配置されたら、通常の方法で操作できます。この例では、SaveChanges を呼び出す前に TaggedBy ペイロード プロパティを設定します。

ここでは、ナビゲーション プロパティの変更を検出し、ChangeTracker.DetectChanges()が使用される前に結合エンティティ インスタンスを作成できるようにするために、Findの呼び出しが必要であることに注意してください。 詳細については、「 変更の検出と通知 」を参照してください。

または、投稿をタグに関連付けるために、結合エンティティを明示的に作成することもできます。 例えば次が挙げられます。

using var context = new BlogsContext();

var post = context.Posts.SingleAsync(e => e.Id == 3);
var tag = context.Tags.SingleAsync(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

最後に、ペイロード データを設定するもう 1 つの方法は、 SaveChanges をオーバーライドするか、 DbContext.SavingChanges イベントを使用してデータベースを更新する前にエンティティを処理することです。 例えば次が挙げられます。

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return await base.SaveChangesAsync(cancellationToken);
}