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

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

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.BlogBlog.Posts の逆ナビゲーションです。
    • Blog.Posts は、ブログから関連付けられているすべての投稿へのコレクション ナビゲーションです。 Blog.PostsPost.Blog の逆ナビゲーションです。
  • 各ブログには、1 つのアセットを関連付けることができます (1 対 1)。
    • Blog はプリンシパル/親です。
    • BlogAssets は依存/子です。 FK プロパティ BlogAssets.BlogId が含まれており、その値は関連ブログの Blog.Id PK 値と一致している必要があります。
    • BlogAssets.Blog は、アセットから関連付けられているブログへの参照ナビゲーションです。 BlogAssets.BlogBlog.Assets の逆ナビゲーションです。
    • Blog.Assets は、ブログから関連付けられているアセットへの参照ナビゲーションです。 Blog.AssetsBlogAssets.Blog の逆ナビゲーションです。
  • 各投稿には多数のタグを関連付けることができ、各タグには多数の投稿を関連付けることができます (多対多)。
    • 多対多リレーションシップは、2 つの 1 対多リレーションシップの上に追加されるレイヤーです。 多対多リレーションシップについては、このドキュメントの後半で説明します。
    • Post.Tags は、投稿から関連付けられているすべてのタグへのコレクション ナビゲーションです。 Post.TagsTag.Posts の逆ナビゲーションです。
    • Tag.Posts は、タグから関連付けられているすべての投稿へのコレクション ナビゲーションです。 Tag.PostsPost.Tags の逆ナビゲーションです。

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

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

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

クエリによる修正

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

using var context = new BlogsContext();

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

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 コレクション ナビゲーションには 2 つの関連する投稿が含まれ、その主キーはそれぞれ 1 と 2 であることを示しています。 同様に、最初のブログに関連付けられている各投稿の Blog: {Id: 1} 行は、Post.Blog ナビゲーションが主キー 1 のブログを参照していることを示しています。

ローカルで追跡されるエンティティに対する修正

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

using var context = new BlogsContext();

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

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

var posts = context.Posts.ToList();
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: []

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

Note

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

ナビゲーションを使用したリレーションシップの変更

2 つのエンティティ間のリレーションシップを変更する最も簡単な方法は、ナビゲーションを操作する一方で、逆ナビゲーションと FK 値の適切な修正を EF Core に任せることです。 これは以下のいずれかの方法で実行できます。

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

コレクション ナビゲーションに対する追加または削除

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

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(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);

context.SaveChanges();

ヒント

デバッグ ビューにアクセスしても変更の自動検出は行われないため、ここでは 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}) を指すように修正されています。 また、Post.BlogId 外部キー値が、.NET ブログの主キー値と一致するように更新されています。 この 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 値を操作するようなコードを記述しないでください。 そのようなコードはより複雑になり、すべてのケースで外部キーとナビゲーションに対して一貫性のある変更を行う必要があります。 可能であれば、1 つのナビゲーションまたは両方のナビゲーションのみを操作してください。 必要な場合は、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);

この変更の後でデバッグ ビューを確認すると、次のことがわかります。

  • 投稿が Deleted としてマークされています。そのため、SaveChanges が呼び出されたときにデータベースから削除されます。
  • 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: []

Post.BlogId は変更されていないことに注目してください。必須リレーションシップの場合、null に設定することはできないためです。

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

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

孤立の削除のタイミングと再ペアレンティング

既定では、リレーションシップの変更が検出されるとすぐに、孤立が Deleted としてマークされます。 ただし、このプロセスは、SaveChanges が実際に呼び出されるまで遅らせることができます。 これは、1 つのプリンシパル/親から削除されたエンティティが、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);

context.SaveChanges();

最初のコレクションから投稿を削除した後、オブジェクトは前の例のように Deleted としてマークされていません。 代わりに、"これは必須リレーションシップであるにもかかわらず"、EF Core ではリレーションシップの切断が追跡されています。 (FK 値は、EF Core では null と見なされますが、型が null 許容ではないため、実際に 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 = context.Blogs.Include(e => e.Posts).Single(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);

context.SaveChanges(); // Throws

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

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

孤立の削除とカスケード削除は、ChangeTracker.CascadeChanges() を呼び出すことで、いつでも強制的に行うことができます。 これに組み合わせて孤立の削除のタイミングを Never に設定すると、EF Core に明示的に指示されない限り、孤立が削除されることはなくなります。

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

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

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

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

using var context = new BlogsContext();

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

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

context.SaveChanges();

SaveChanges を呼び出す前のデバッグ ビューでは、既存のアセットが新しいアセットで置き換えられて現在は Modified としてマークされており、その BlogAssets.BlogId FK 値は null になっていることがわかります。

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 リレーションシップで実行すると、前に関連付けられていた BlogAssets が現在は Deleted としてマークされていることがわかります。これは、新しい 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 = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

context.SaveChanges();

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>) があります。

必須リレーションシップ

必須リレーションシップの修正動作は、省略可能なリレーションシップの場合と同じですが、依存/子エンティティは Deleted としてマークされます。これらはプリンシパル/親なしでは存在できず、SaveChanges が呼び出されたときにデータベースから削除して、参照制約の例外が発生しないようにする必要があるためです。 これは "カスケード削除" と呼ばれ、必須リレーションシップに対する 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 としてマークされるとすぐに、カスケード削除が行われます。 これは、前に説明した孤立の削除の場合と同じです。 孤立の削除の場合と同様に、ChangeTracker.CascadeDeleteTiming を適切に設定することで、SaveChanges が呼び出されるまでこのプロセスを遅らせることができ、完全に無効にすることもできます。 これは、プリンシパル/親の削除後に子/依存を再ペアレンティングする場合などに、孤立の削除の場合と同じしくみで役立ちます。

カスケード削除と孤立の削除は、ChangeTracker.CascadeChanges() を呼び出すことで、いつでも強制的に行うことができます。 これに組み合わせてカスケード削除のタイミングを Never に設定すると、EF Core に明示的に指示されない限り、カスケード削除が行われることはなくなります。

ヒント

カスケード削除と孤立の削除は密接に関連しています。 どちらの場合も、必須のプリンシパルまたは親とのリレーションシップが切断されると、依存または子エンティティが削除されます。 カスケード削除の場合、この切断は、プリンシパルまたは親自体が削除されたことが原因で発生します。 孤立の場合、プリンシパルまたは親エンティティは依然として存在しますが、依存または子エンティティとの関連付けはなくなっています。

多対多関連付け

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

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

明示的に定義された結合エンティティ型を使用して投稿とタグの間に多対多のリレーションシップを作成する、次の 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 結合エンティティが存在し、その PostTag.PostId 外部キー値が Post.Id 主キー値と一致すること、および PostTag.TagId 外部キー値が Tag.Id 主キー値と一致することが必要です。 次に例を示します。

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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}]

PostTag の参照ナビゲーションと同様に、PostTag のコレクション ナビゲーションが修正されている点に注目してください。 前出のすべての例と同様に、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));
}

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

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

using var context = new BlogsContext();

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

post.Tags.Add(tag);

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

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

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}]

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

基になる多対多リレーションシップは、その上にスキップ ナビゲーションがレイヤー化されている場合でも、引き続き直接操作できます。 たとえば、スキップ ナビゲーションの導入以前と同様の方法で、タグと投稿を関連付けることができます。

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

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

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

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

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

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

using var context = new BlogsContext();

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

post.Tags.Add(tag);

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

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

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> エンティティ型の使用の詳細については、「リレーションシップ」を参照してください。

重要

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

ペイロードを持つ結合エンティティ

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

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

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

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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

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

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

Note

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

または、投稿をタグに関連付ける結合エンティティを明示的に作成することもできます。 次に例を示します。

using var context = new BlogsContext();

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

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

context.SaveChanges();

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

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

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}