Teilen über


Ändern von Fremdschlüsseln und Navigationen

Übersicht über Fremdschlüssel und Navigationen

Beziehungen in einem Entity Framework Core (EF Core)-Modell werden mithilfe von Fremdschlüsseln (FKs) dargestellt. Ein FK besteht aus einer oder mehreren Eigenschaften der abhängigen oder untergeordneten Entität in der Beziehung. Diese abhängige/untergeordnete Entität wird einer bestimmten Prinzipal-/übergeordneten Entität zugeordnet, wenn die Werte der Fremdschlüsseleigenschaften des abhängigen/untergeordneten Elements mit den Werten der Alternativen oder Primärschlüsseleigenschaften (PK) des Prinzipals/übergeordneten Elements übereinstimmen.

Fremdschlüssel sind eine gute Möglichkeit zum Speichern und Bearbeiten von Beziehungen in der Datenbank, sind aber beim Arbeiten mit mehreren verwandten Entitäten im Anwendungscode nicht sehr benutzerfreundlich. Daher legen die meisten EF Core-Modelle auch „Navigationen“ über die FK-Darstellung. Navigationsformulare bilden C#/.NET-Verweise zwischen Entitätsinstanzen, welche die Zuordnungen widerspiegeln, die durch übereinstimmende Fremdschlüsselwerte zu primären oder alternativen Schlüsselwerten gefunden wurden.

Navigationen können auf beiden Seiten der Beziehung, nur auf einer Seite oder gar nicht verwendet werden, sodass nur die FK-Eigenschaft übrig bleibt. Die FK-Eigenschaft kann ausgeblendet werden, indem sie zu einer Schatteneigenschaft wird. Weitere Informationen zum Modellieren von Beziehungen finden Sie unter Beziehungen.

Tipp

In diesem Dokument wird davon ausgegangen, dass Entitätszustände und die Grundlagen der EF Core-Änderungsnachverfolgung verstanden werden. Weitere Informationen zu diesen Themen finden Sie unter Änderungsnachverfolgung in EF Core.

Tipp

Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Beispielmodell

Das folgende Modell enthält vier Entitätstypen mit Beziehungen untereinander. Die Kommentare im Code geben an, welche Eigenschaften Fremdschlüssel, Primärschlüssel und Navigationen sind.

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
}

Die drei Beziehungen in diesem Modell sind:

  • Jeder Blog kann viele Beiträge haben (1:n):
    • Blog ist der Prinzipal/das übergeordnete Element.
    • Post ist das abhängige/untergeordnete Element. Es enthält die FK-Eigenschaft Post.BlogId, dessen Wert dem PK-Wert Blog.Id des zugehörigen Blogs entsprechen muss.
    • Post.Blog ist eine Referenznavigation von einem Beitrag zum zugehörigen Blog. Post.Blog ist die umgekehrte Navigation für Blog.Posts.
    • Blog.Posts ist eine Sammlungsnavigation von einem Blog zu allen zugehörigen Beiträgen. Blog.Posts ist die umgekehrte Navigation für Post.Blog.
  • Jeder Blog kann über ein Objekt (1:1) verfügen:
    • Blog ist der Prinzipal/das übergeordnete Element.
    • BlogAssets ist das abhängige/untergeordnete Element. Es enthält die FK-Eigenschaft BlogAssets.BlogId, dessen Wert dem PK-Wert Blog.Id des zugehörigen Blogs entsprechen muss.
    • BlogAssets.Blog ist eine Referenznavigation von den Objekten zum zugehörigen Blog. BlogAssets.Blog ist die umgekehrte Navigation für Blog.Assets.
    • Blog.Assets ist eine Referenznavigation aus dem Blog zu den zugehörigen Objekten. Blog.Assets ist die umgekehrte Navigation für BlogAssets.Blog.
  • Jeder Beitrag kann viele Tags enthalten, und jedes Tag kann viele Beiträge enthalten (m:n):
    • m:n-Beziehungen sind eine weitere Ebene über zwei 1:n-Beziehungen. m:n-Beziehungen werden weiter unten in diesem Dokument behandelt.
    • Post.Tags ist eine Sammlungsnavigation von einem Beitrag zu allen zugeordneten Tags. Post.Tags ist die umgekehrte Navigation für Tag.Posts.
    • Tag.Posts ist eine Sammlungsnavigation von einem Tag zu allen zugehörigen Beiträgen. Tag.Posts ist die umgekehrte Navigation für Post.Tags.

Weitere Informationen zum Modellieren und Konfigurieren von Beziehungen finden Sie unter Beziehungen.

Beziehungskorrektur

EF Core hält Navigationen in Übereinstimmung mit Fremdschlüsselwerten und umgekehrt. Das heißt, wenn sich ein Fremdschlüsselwert so ändert, dass er sich jetzt auf eine andere Prinzipal-/übergeordnete Entität bezieht, werden die Navigationen aktualisiert, um diese Änderung widerzuspiegeln. Ebenso werden die Fremdschlüsselwerte der beteiligten Entitäten aktualisiert, wenn eine Navigation geändert wird, um diese Änderung widerzuspiegeln. Dies wird als „Beziehungskorrektur“ bezeichnet.

Korrektur nach Abfrage

Die Korrektur tritt zuerst auf, wenn Entitäten aus der Datenbank abgefragt werden. Die Datenbank enthält nur Fremdschlüsselwerte. Wenn EF Core also eine Entitätsinstanz aus der Datenbank erstellt, verwendet sie die Fremdschlüsselwerte, um Referenznavigationen festzulegen und entsprechende Entitäten zu Sammlungsnavigationen hinzuzufügen. Ziehen Sie beispielsweise eine Abfrage für Blogs und die zugehörigen Beiträge und Ressourcen in Betracht:

using var context = new BlogsContext();

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

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

Für jeden Blog erstellt EF Core zunächst eine Instanz Blog. Da dann jeder Beitrag aus der Datenbank geladen wird, wird die Referenznavigation Post.Blog so festgelegt, dass sie auf den zugehörigen Blog verweist. Ebenso wird der Beitrag der Sammlungsnavigation Blog.Posts hinzugefügt. Dasselbe geschieht mit BlogAssets, außer dass in diesem Fall beide Navigationselemente Verweise sind. Die Blog.Assets-Navigation wird so festgelegt, dass sie auf die Ressourceninstanz zeigt, und die BlogAsserts.Blog-Navigation wird so festgelegt, dass sie auf die Bloginstanz verweist.

Wenn Sie die Debugansicht der Änderungsverfolgung nach dieser Abfrage betrachten, werden zwei Blogs angezeigt, wobei jeweils ein Objekt und zwei Beiträge nachverfolgt werden:

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: []

In der Debugansicht werden sowohl Schlüsselwerte als auch Navigationen angezeigt. Navigationen werden mithilfe der Primärschlüsselwerte der zugehörigen Entitäten angezeigt. In der Posts: [{Id: 1}, {Id: 2}] obigen Ausgabe wird beispielsweise angegeben, dass die Blog.Posts Sammlungsnavigation zwei verwandte Beiträge mit Primärschlüsseln 1 bzw. 2 enthält. Ebenso weist die Zeile Blog: {Id: 1} für jeden Beitrag, der dem ersten Blog zugeordnet ist, darauf hin, dass die Navigation Post.Blog auf den Blog mit Primärschlüssel 1 verweist.

Korrektur für lokal nachverfolgte Entitäten

Die Beziehungskorrektur erfolgt auch zwischen Entitäten, die von einer Nachverfolgungsabfrage zurückgegeben werden, und Entitäten, die bereits vom DbContext nachverfolgt wurden. Erwägen Sie beispielsweise das Ausführen von drei separaten Abfragen für Blogs, Beiträge und Ressourcen:

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);

Wenn Sie sich die Debugansichten erneut ansehen, werden nach der ersten Abfrage nur die beiden Blogs nachverfolgt:

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: []

DieBlog.Assets-Referenznavigationen sind NULL, und die Blog.Posts-Sammlungsnavigationen sind leer, da derzeit keine zugeordneten Entitäten vom Kontext nachverfolgt werden.

Nach der zweiten Abfrage wurden die Blogs.Assets-Referenznavigationen bis zu den neu nachverfolgten BlogAsset-Instanzen korrigiert. Ebenso werden die BlogAssets.Blog-Referenznavigationen so festgelegt, dass sie auf die entsprechende bereits nachverfolgte Blog-Instanz verweisen.

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}

Schließlich enthalten die Blog.Posts-Sammlungsnavigationen nach der dritten Abfrage nun alle verwandten Beiträge, und die Post.Blog-Verweise verweisen auf die entsprechende Blog-Instanz:

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: []

Dies ist derselbe Endzustand wie bei der ursprünglichen einzelnen Abfrage, da korrigierte EF Core Navigationen wie Entitäten nachverfolgt wurden, auch wenn sie aus mehreren verschiedenen Abfragen stammen.

Hinweis

Eine Korrektur bewirkt niemals, dass mehr Daten aus der Datenbank zurückgegeben werden. Es verbindet nur Entitäten, die bereits von der Abfrage zurückgegeben oder bereits vom DbContext nachverfolgt werden. Informationen zum Behandeln von Duplikaten beim Serialisieren von Entitäten finden Sie unter Identity Resolution in EF Core.

Ändern von Beziehungen mithilfe von Navigationen

Die einfachste Möglichkeit, die Beziehung zwischen zwei Entitäten zu ändern, besteht darin, eine Navigation zu bearbeiten, während EF Core die umgekehrten Navigations- und FK-Werte entsprechend korrigiert. Dies kann durch Folgendes geschehen:

  • Hinzufügen oder Entfernen einer Entität aus einer Sammlungsnavigation.
  • Ändern einer Referenznavigation, um auf eine andere Entität zu verweisen, oder sie auf NULL zu setzen.

Hinzufügen oder Entfernen von Sammlungsnavigationen

Verschieben wir beispielsweise einen der Beiträge aus dem Visual Studio-Blog in den .NET-Blog. Dazu müssen zuerst die Blogs und Beiträge geladen und dann der Beitrag aus der Navigationssammlung in einem Blog in die Navigationssammlung im anderen Blog verschoben werden:

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

Tipp

Hier wird ein Aufruf von ChangeTracker.DetectChanges() benötigt, da der Zugriff auf die Debugansicht keine automatische Erkennung von Änderungen verursacht.

Dies ist die Debugansicht, die nach dem Ausführen des obigen Codes gedruckt wird:

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: []

Die Navigation Blog.Posts im .NET-Blog hat jetzt drei Beiträge (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Ebenso hat die Navigation Blog.Posts im Visual Studio-Blog nur einen Beitrag (Posts: [{Id: 4}]). Dies ist zu erwarten, da der Code diese Sammlungen explizit geändert hat.

Interessanter ist, obwohl wenn der Code die Navigation Post.Blog nicht explizit geändert hat, wurde er so korrigiert, dass er auf den Visual Studio-Blog (Blog: {Id: 1}) verweist. Außerdem wurde der Post.BlogId Fremdschlüsselwert aktualisiert, um dem Primärschlüsselwert des .NET-Blogs zu entsprechen. Diese Änderung des FK-Werts wird dann in der Datenbank beibehalten, wenn SaveChanges aufgerufen wird:

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

Ändern von Referenznavigationen

Im vorherigen Beispiel wurde ein Beitrag von einem Blog in einen anderen verschoben, indem die Sammlungsnavigation von Beiträgen in jedem Blog manipuliert wurde. Dasselbe kann erreicht werden, indem stattdessen die Referenznavigation Post.Blog so geändert wird, dass sie auf den neuen Blog verweist. Beispiel:

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

Die Debugansicht nach dieser Änderung entspricht genau dergleichen wie im vorherigen Beispiel. Dies liegt daran, dass EF Core die Änderung der Referenznavigation erkannt und dann die Sammlungsnavigation und den FK-Wert, der übereinstimmen soll, korrigiert hat.

Ändern von Beziehungen mithilfe von Fremdschlüsselwerten

Im vorherigen Abschnitt wurden Beziehungen durch Navigationen bearbeitet, die Fremdschlüsselwerte automatisch aktualisieren lassen. Das ist die empfohlene Methode zum Bearbeiten von Beziehungen in EF Core. Es ist jedoch auch möglich, FK-Werte direkt zu bearbeiten. Beispielsweise können wir einen Beitrag von einem Blog in einen anderen verschieben, indem wir den Fremdschlüsselwert-Post.BlogId ändern:

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

Beachten Sie, dass dies dem Ändern der Referenznavigation, die im vorherigen Beispiel gezeigt wurde, ähnlich ist.

Die Debugansicht nach dieser Änderung ist wieder identisch mit den beiden vorherigen Beispielen. Dies liegt daran, dass EF Core die FK-Wertänderung erkannt und dann sowohl die Referenz- als auch die Sammlungsnavigationen korrigiert hat, damit sie übereinstimmen.

Tipp

Schreiben Sie keinen Code, um alle Navigations- und FK-Werte bei jeder Änderung einer Beziehung zu bearbeiten. Dieser Code ist komplizierter und muss in jedem Fall konsistente Änderungen an Fremdschlüsseln und Navigationen sicherstellen. Wenn möglich, bearbeiten Sie einfach eine einzelne Navigation oder vielleicht beide Navigationen. Bearbeiten Sie bei Bedarf einfach die FK-Werte. Vermeiden Sie das gleichzeitige Bearbeiten von Navigations- und FK-Werten.

Korrektur für hinzugefügte oder gelöschte Entitäten

Hinzufügen zu einer Sammlungsnavigation

EF Core führt die folgenden Aktionen aus, wenn erkannt wird, dass einer Sammlungsnavigation eine neue abhängige/untergeordnete Entität hinzugefügt wurde:

  • Wenn die Entität nicht nachverfolgt wird, wird diese nachverfolgt. (Die Entität befindet sich in der Regel im Zustand Added. Wenn der Entitätstyp jedoch für die Verwendung generierter Schlüssel konfiguriert ist und der Primärschlüsselwert festgelegt wird, wird die Entität im Status Unchanged nachverfolgt.)
  • Wenn die Entität einem anderen Prinzipal/übergeordneten Element zugeordnet ist, wird diese Beziehung getrennt.
  • Die Entität wird dem Prinzipal/übergeordneten Element zugeordnet, das die Sammlungsnavigation besitzt.
  • Navigations- und Fremdschlüsselwerte werden bei allen beteiligten Entitäten korrigiert.

Auf dieser Grundlage können wir sehen, dass wir, um einen Beitrag von einem Blog in einen anderen zu verschieben, wir ihn gar nicht aus der alten Sammlungsnavigation entfernen müssen, bevor wir ihn der neuen hinzufügen. Der Code aus dem obigen Beispiel kann also wie folgt geändert werden:

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

In:

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

EF Core sieht, dass der Beitrag zu einem neuen Blog hinzugefügt und automatisch aus der Sammlung des ersten Blogs entfernt wurde.

Entfernen aus einer Sammlungsnavigation

Durch das Entfernen einer abhängigen/untergeordneten Entität aus der Sammlungsnavigation des Prinzipals/übergeordneten Elements wird die Beziehung zu diesem Prinzipal/übergeordneten Element getrennt. Was als nächstes geschieht, hängt davon ab, ob die Beziehung optional oder erforderlich ist.

Optionale Beziehungen

Der Fremdschlüsselwert ist standardmäßig für optionale Beziehungen auf NULL festgelegt. Das bedeutet, dass das abhängige/untergeordnete Element keinem Prinzipal/übergeordneten Element mehr zugeordnet ist. Lassen Sie uns beispielsweise einen Blog und Beiträge laden und dann einen der Beiträge aus der Sammlungsnavigation Blog.Posts entfernen:

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

Wenn Sie sich die Debugansicht für die Änderungsnachverfolgung ansehen, wird Folgendes angezeigt:

  • Der FK Post.BlogId wurde auf NULL (BlogId: <null> FK Modified Originally 1) gesetzt
  • Die Referenznavigation Post.Blog wurde auf NULL (Blog: <null>) gesetzt
  • Der Beitrag wurde aus der Sammlungsnavigation Blog.Posts entfernt (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: []

Beachten Sie, dass der Beitrag nicht als Deleted gekennzeichnet wurde. Er ist als Modified gekennzeichnet, damit der FK-Wert in der Datenbank beim Aufrufen von SaveChanges auf NULL gesetzt wird.

Erforderliche Beziehungen

Das Festlegen des FK-Werts auf NULL ist für erforderliche Beziehungen nicht zulässig (und ist in der Regel auch nicht möglich). Daher bedeutet das Abtrennen einer erforderlichen Beziehung, dass die abhängige/untergeordnete Entität entweder einem neuen Prinzipal/übergeordneten Element zugeordnet oder aus der Datenbank entfernt werden muss, wenn SaveChanges aufgerufen wird, um eine Verletzung referenzieller Einschränkungen zu vermeiden. Dies wird als „Löschen von Verwaisten“ bezeichnet und ist das Standardverhalten in EF Core für erforderliche Beziehungen.

Ändern wir beispielsweise die Beziehung zwischen Blog und Beiträgen, die erforderlich sind, und führen dann denselben Code wie im vorherigen Beispiel aus:

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

Wenn Sie die Debugansicht nach dieser Änderung betrachten, wird Folgendes angezeigt:

  • Der Beitrag wurde als Deleted markiert, sodass er aus der Datenbank gelöscht wird, wenn SaveChanges aufgerufen wird.
  • Die Referenznavigation Post.Blog wurde auf NULL (Blog: <null>) gesetzt.
  • Der Beitrag wurde aus der Sammlungsnavigation Blog.Posts entfernt (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: []

Beachten Sie, dass der Wert Post.BlogId unverändert bleibt, da er für eine erforderliche Beziehung nicht auf NULL gesetzt werden kann.

Das Aufrufen von SaveChanges führt dazu, dass der verwaiste Beitrag gelöscht wird:

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

Löschen der Anzeigedauer für verwaiste Elemente und erneute Zuordnung eines übergeordneten Elements

Standardmäßig erfolgt das Markieren von Verwaisten als Deleted sobald die Beziehungsänderung erkannt wird. Dieser Vorgang kann jedoch verzögert werden, bis SaveChanges tatsächlich aufgerufen wird. Dies kann hilfreich sein, um verwaiste Entitäten zu vermeiden, die aus einem Prinzipal/übergeordneten Element entfernt wurden, aber vor dem Aufrufen von SaveChanges erneut mit einem neuen Prinzipal/übergeordneten Element versehen werden. ChangeTracker.DeleteOrphansTiming wird verwendet, um diese Anzeigedauer festzulegen. Beispiel:

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

Nach dem Entfernen des Beitrags aus der ersten Auflistung wird das Objekt nicht als Deleted, wie im vorherigen Beispiel, markiert. Stattdessen verfolgt EF Core nach, dass die Beziehung getrennt wird, selbst wenn es eine erforderliche Beziehung ist. (Der FK-Wert wird von EF Core als NULL betrachtet, obwohl er nicht wirklich NULL sein kann, da der Typ nicht nullfähig ist. Dies wird als "konzeptionelle NULL" bezeichnet.)

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: []

Das Aufrufen von SaveChanges zu diesem Zeitpunkt würde dazu führen, dass der verwaiste Beitrag gelöscht wird. Wenn der Beitrag wie im obigen Beispiel jedoch einem neuen Blog zugeordnet ist, bevor SaveChanges aufgerufen wird, wird er entsprechend dem neuen Blog korrigiert und gilt nicht mehr als verwaist:

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: []

Wenn SaveChanges an diesem Punkt aufgerufen wird, wird der Beitrag in der Datenbank aktualisiert anstatt gelöscht.

Es ist auch möglich, das automatische Löschen von Verwaisten zu deaktivieren. Dies führt zu einer Ausnahme, wenn SaveChanges aufgerufen wird, während ein Verwaister nachverfolgt wird. Ein Beispiel ist der folgende Code:

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

Löst diese Ausnahme aus:

System.InvalidOperationException: Die Zuordnung zwischen Entitäten ‚Blog‘ und ‚Post‘ mit dem Schlüsselwert ‚{BlogId: 1}‘ wurde getrennt, die Beziehung ist jedoch entweder als erforderlich markiert oder implizit erforderlich, da der Fremdschlüssel nicht nullfähig ist. Wenn die abhängige/untergeordnete Entität gelöscht werden soll, wenn eine erforderliche Beziehung getrennt wird, konfigurieren Sie die Beziehung so, dass Löschweitergaben verwendet werden.

Das Löschen von Verwaisten sowie Löschweitergaben kann jederzeit durch Aufrufen von ChangeTracker.CascadeChanges() erzwungen werden. Wenn Sie diese Einstellung mit dem Waisenlöschzeitpunkts Never kombinieren, wird sichergestellt, dass Verwaiste niemals gelöscht werden, es sei denn, EF Core wird explizit angewiesen, dies zu tun.

Ändern einer Referenznavigation

Das Ändern der Referenznavigation einer 1:n-Beziehung hat die gleiche Auswirkung wie das Ändern der Sammlungsnavigation am anderen Ende der Beziehung. Das Festlegen der Referenznavigation abhängiger/untergeordneter Elemente auf NULL entspricht dem Entfernen der Entität aus der Sammlungsnavigation des Prinzipals/übergeordneten Elements. Alle Korrektur- und Datenbankänderungen erfolgen, wie im vorherigen Abschnitt beschrieben, einschließlich der Erstellung der Entität als verwaist, wenn die Beziehung erforderlich ist.

Optionale 1:1-Beziehungen

Bei 1:1-Beziehungen wird beim Ändern einer Referenznavigation jede vorherige Beziehung getrennt. Bei optionalen Beziehungen bedeutet dies, dass der FK-Wert für die zuvor verknüpften abhängigen/untergeordneten Elemente auf NULL festgelegt ist. Beispiel:

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

Die Debugansicht vor dem Aufrufen von SaveChanges zeigt, dass die neuen Ressourcen die vorhandenen Ressourcen ersetzt haben, welche jetzt als Modified NULL-FK-Wert BlogAssets.BlogId markiert sind:

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>

Dies führt zu einer Aktualisierung und einem Einfügen, wenn SaveChanges aufgerufen wird:

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

Erforderliche 1:1-Beziehungen

Wenn Sie den gleichen Code wie im vorherigen Beispiel ausführen, dieses Mal jedoch mit einer erforderlichen 1:1-Beziehung, wird gezeigt, dass die zuvor zugeordnete BlogAssets jetzt als Deleted markiert ist, da sie zu einem Verwaisten wird, wenn das neue BlogAssets ausgeführt wird:

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>

Dies führt dann zu einem Löschen und einem Einfügen, wenn SaveChanges aufgerufen wird:

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

Die Anzeigedauer der Markierung von Verwaisten als gelöscht kann auf die gleiche Weise wie bei Sammlungsnavigationen geändert werden und hat dieselben Auswirkungen.

Löschen einer Entität

Optionale Beziehungen

Wenn eine Entität z. B. durch Aufrufen von DbContext.Remove als Deleted gekennzeichnet wird, werden Verweise auf die gelöschte Entität aus den Navigationen anderer Entitäten entfernt. Bei optionalen Beziehungen werden die FK-Werte in abhängigen Entitäten auf NULL festgelegt.

Markieren wir beispielsweise den Visual Studio-Blog als 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();

Sehen Sie sich die Debugansicht der Änderungsverfolgung vor dem Aufrufen von SaveChanges an:

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: []

Beachten Sie Folgendes:

  • Der Blog ist als Deleted markiert.
  • Die Objekte im Zusammenhang mit dem gelöschten Blog haben einen Null-FK-Wert (BlogId: <null> FK Modified Originally 2) und eine NULL-Referenznavigation (Blog: <null>)
  • Jeder Beitrag im Zusammenhang mit dem gelöschten Blog weist einen NULL-FK-Wert (BlogId: <null> FK Modified Originally 2) und eine Nullverweisnavigation (Blog: <null>) auf

Erforderliche Beziehungen

Das Korrekturverhalten für erforderliche Beziehungen ist dasselbe wie für optionalen Beziehungen, mit der Ausnahme, dass die abhängigen/untergeordneten Entitäten als Deleted gekennzeichnet sind, da sie nicht ohne einen Prinzipal/übergeordnetes Element existieren können und aus der Datenbank entfernt werden müssen, wenn SaveChanges aufgerufen wird, um eine Ausnahme für referenzielle Einschränkungen zu vermeiden. Dies wird als „Löschweitergabe“ bezeichnet und ist das Standardverhalten in EF Core für erforderliche Beziehungen. Beispielsweise führt das Ausführen desselben Codes wie im vorherigen Beispiel, aber mit einer erforderlichen Beziehung zu der folgenden Debugansicht, bevor SaveChanges aufgerufen wird:

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: []

Wie erwartet, sind die Nachfolger/untergeordneten Elemente jetzt als Deleted gekennzeichnet. Beachten Sie jedoch, dass sich die Navigationen in den gelöschten Entitäten nicht geändert haben. Dies mag seltsam erscheinen, aber es verhindert, dass ein gelöschtes Diagramm von Entitäten vollständig zerstört wird, indem alle Navigationen gelöscht werden. Das heißt, der Blog, die Ressource und die Beiträge bilden weiterhin ein Diagramm von Entitäten, auch nachdem sie gelöscht wurden. Dadurch ist es viel einfacher, ein Diagramm von Entitäten wieder herzustellen, als in EF6 der Fall war, wo das Diagramm zerstört wurde.

Löschdauer und erneutes Zuordnen eines übergeordneten Elements

Standardmäßig erfolgt die Löschweitergabe, sobald das übergeordnete Element/Prinzipal als Deleted markiert ist. Dies ist identisch mit dem zuvor beschriebenen Löschen von Verwaisten. Wie beim Löschen von Verwaisten kann dieser Vorgang verzögert werden, bis SaveChanges aufgerufen oder sogar vollständig deaktiviert wird, indem ChangeTracker.CascadeDeleteTiming entsprechend festgelegt wird. Dies ist auf die gleiche Weise hilfreich wie beim Löschen von Verwaisten, einschließlich des erneutes Zuordnen von untergeordneten Elementen/Abhängigen nach dem Löschen eines Prinzipals/übergeordneten Elements.

Löschweitergaben sowie das Löschen von Verwaisten können jederzeit durch Aufrufen von ChangeTracker.CascadeChanges() erzwungen werden. Wenn Sie dies mit dem Einstellen der Anzeigedauer für die Löschweitergabe auf Never kombinieren, wird sichergestellt, dass Löschweitergaben niemals auftreten, es sei denn, EF Core wird explizit angewiesen, sie auszuführen.

Tipp

Das kaskadierende Delete und das Löschen verwaister Entitäten sind eng verwandt. Beide führen dazu, dass abhängige/untergeordnete Entitäten gelöscht werden, wenn die Beziehung zum erforderlichen Prinzipal bzw. der übergeordneten Entität getrennt wird. Beim kaskadierenden Delete erfolgt diese Trennung, weil der Prinzipal bzw. die übergeordnete Entität selbst gelöscht wird. Bei verwaisten Entitäten ist der Prinzipal bzw. die übergeordnete Entität weiterhin vorhanden, jedoch besteht die Beziehung zur abhängigen/untergeordneten Entität nicht mehr.

Viele-zu-viele-Beziehungen

m:n-Beziehungen in EF Core werden mithilfe einer Verknüpfungsentität implementiert. Jede Seite der m:n-Beziehung ist mit dieser Verknüpfungsentität mit einer 1:n-Beziehung verknüpft. Diese Verknüpfungsentität kann explizit definiert und zugeordnet werden, oder sie kann implizit und ausgeblendet erstellt werden. In beiden Fällen ist das zugrunde liegende Verhalten identisch. Wir werden dieses zugrunde liegende Verhalten zuerst untersuchen, um zu verstehen, wie die Nachverfolgung von m:n-Beziehungen funktioniert.

Wie m:n-Beziehungen funktionieren

Betrachten Sie dieses EF Core-Modell, das eine m:n-Beziehung zwischen Beiträgen und Tags mithilfe eines explizit definierten Verknüpfungsentitätstyps erstellt:

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
}

Beachten Sie, dass der Verknüpfungsentitätstyp PostTag zwei Fremdschlüsseleigenschaften enthält. In diesem Modell muss für einen Beitrag, der mit einem Tag verknüpft ist, eine Beitrag-Tag-Verknüpfungsentität vorhanden sein, bei welcher der Fremdschlüsselwert PostTag.PostId mit dem Primärschlüsselwert Post.Id übereinstimmt und wo der Fremdschlüsselwert PostTag.TagId mit dem Primärschlüsselwert Tag.Id übereinstimmt. Beispiel:

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);

Wenn Sie sich die Debugansicht der Änderungsverfolgung ansehen, nachdem dieser Code ausgeführt wurde, wird gezeigt, dass der Beitrag und das Tag von der neuen Verknüpfungsentität PostTag verknüpft wurden:

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

Beachten Sie, dass die Sammlungsnavigationen auf Post und Tag korrigiert wurde, sowie die Referenznavigationen auf PostTag. Diese Beziehungen können von Navigationen anstelle von FK-Werten bearbeitet werden, genau wie in allen vorherigen Beispielen. Beispielsweise kann der obige Code geändert werden, um die Beziehung hinzuzufügen, indem die Referenznavigationen auf die Verknüpfungsentität festgelegt werden:

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

Dies führt zu genau der gleichen Änderung an FKs und Navigationen wie im vorherigen Beispiel.

Navigation überspringen

Das manuelle Bearbeiten der Verknüpfungstabelle kann umständlich sein. m:n-Beziehungen können direkt mithilfe spezieller Sammlungsnavigationen bearbeitet werden, welche die Verknüpfungsentität „überspringen“. So können dem obigen Modell beispielsweise zwei Übersprungnavigationen hinzugefügt werden; eine von Beitrag zu Tags und die andere von Tag zu Beiträge:

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
}

Für diese m:n-Beziehung ist die folgende Konfiguration erforderlich, um sicherzustellen, dass Übersprungnavigationen und normale Navigationen für dieselbe m:n-Beziehung verwendet werden:

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

Weitere Informationen zum Zuordnen von m:n-Beziehungen finden Sie unter Beziehungen.

Übersprungnavigationen sehen aus imd verhalten sich wie normale Sammlungsnavigationen. Die Art und Weise, wie sie mit Fremdschlüsselwerten arbeiten, unterscheidet sich jedoch. Lassen Sie uns einen Beitrag einem Tag zuordnen, dieses Mal aber mit einer Übersprungnavigation:

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);

Beachten Sie, dass dieser Code nicht die Verknüpfungsentität verwendet. Stattdessen wird einer Navigationssammlung eine Entität auf die gleiche Weise hinzugefügt wie bei einer 1:n-Beziehung. Die resultierende Debugansicht ist im Wesentlichen die gleiche wie zuvor:

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

Beachten Sie, dass automatisch eine Instanz der Verknüpfungsentität PostTag erstellt wurde, wobei die FK-Werte auf die PK-Werte des Tags und des Beitrags festgelegt sind, die jetzt zugeordnet sind. Alle normalen Referenz- und Sammlungsnavigationen wurden so korrigiert, dass sie diesen FK-Werten entsprechen. Da dieses Modell auch Übersprungnavigationen enthält, wurden diese ebenfalls korrigiert. Auch wenn wir das Tag zur Post.Tags Übersprungnavigation hinzugefügt haben, wurde die Tag.Posts umgekehrte Übersprungnavigation auf der anderen Seite dieser Beziehung ebenfalls so korrigiert, dass der zugeordnete Beitrag enthalten ist.

Es lohnt sich zu beachten, dass die zugrunde liegenden m:n-Beziehungen auch dann direkt bearbeitet werden können, wenn Navigationsleisten übereinander angeordnet wurden. Beispielsweise könnte das Tag und der Beitrag wie zuvor zugeordnet werden, bevor wir Übersprungnavigation einführen:

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

Oder verwenden Sie FK-Werte:

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

Dies führt weiterhin dazu, dass die Übersprungnavigationen ordnungsgemäß behoben werden, was zu derselben Debugansichtsausgabe wie im vorherigen Beispiel führt.

Nur Übersprungnavigation

Im vorherigen Abschnitt haben wir zusätzlich zurvollständigen Definition der beiden zugrunde liegenden 1:n-Beziehungen Übersprungnavigation hinzugefügt. Dies ist nützlich, um zu veranschaulichen, was mit FK-Werten geschieht, ist aber häufig unnötig. Stattdessen kann die m:n-Beziehung nur mithilfe von Übersprungnavigationendefiniert werden. So wird die m:n-Beziehung im Modell ganz oben in diesem Dokument definiert. Mithilfe dieses Modells können wir einen Beitrag und ein Tag erneut zuordnen, indem wir der Übersprungnavigation Tag.Posts einen Beitrag hinzufügen (oder alternativ der Übersprungnavigation Post.Tags ein Tag hinzufügen):

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);

Wenn Sie sich die Debugansicht ansehen, nachdem Sie diese Änderung vorgenommen haben, wird angezeigt, dass EF Core eine Instanz von Dictionary<string, object> erstellt hat, welche die Verknüpfungsentität darstellt. Diese Verknüpfungsentität enthält sowohl die Fremdschlüsseleigenschaften von PostsId also auch von TagsId, die so festgelegt wurden, dass sie den PK-Werten des zugeordneten Beitrags und des Tags entsprechen.

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

Weitere Informationen zu impliziten Verknüpfungsentitäten und zur Verwendung von Entitätstypen Dictionary<string, object> finden Sie unter Beziehungen.

Wichtig

Der CLR-Typ, der für die Verknüpfung von Entitätstypen nach Konvention verwendet wird, kann sich in zukünftigen Versionen ändern, um die Leistung zu verbessern. Verlassen Sie sich nicht darauf, dass der Jointyp Dictionary<string, object> ist, es sei denn, dies wurde explizit konfiguriert.

Verbinden von Entitäten mit Nutzlasten

Bisher haben alle Beispiele einen Verknüpfungsentitätstyp (ob explizit oder implizit) verwendet, der nur die beiden Fremdschlüsseleigenschaften enthält, die für die m:n-Beziehung erforderlich sind. Keiner dieser FK-Werte muss explizit von der Anwendung festgelegt werden, wenn Beziehungen manipuliert werden, da ihre Werte aus den Primärschlüsseleigenschaften der verwandten Entitäten stammen. Auf diese Weise kann EF Core Instanzen der Verknüpfungsentität erstellen, ohne dass Daten fehlen.

Nutzlasten mit generierten Werten

EF Core unterstützt das Hinzufügen zusätzlicher Eigenschaften zum Verknüpfungsentitätstyp. Dies wird als der Verknüpfungsentität eine „Payload“ hinzufügen bezeichnet. Nehmen wir beispielsweise die Eigenschaft TaggedOn zur Verknüpfungsentität hinzu 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
}

Diese Nutzlasteigenschaft wird nicht festgelegt, wenn EF Core eine Verknüpfungsentitätsinstanz erstellt. Die am häufigsten verwendete Möglichkeit besteht darin, Nutzlasteigenschaften mit automatisch generierten Werten zu verwenden. Beispielsweise kann die TaggedOn Eigenschaft so konfiguriert werden, dass ein vom Speicher generierter Zeitstempel verwendet wird, jedes mal wenn neue Entität eingefügt wird:

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"));
}

Ein Beitrag kann jetzt auf die gleiche Weise wie zuvor markiert werden:

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);

Wenn Sie sich die Debugansicht der Änderungsverfolgung ansehen, nachdem SaveChanges aufgerufen wurde, wird gezeigt, dass die Nutzlasteigenschaft entsprechend festgelegt wurde:

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

Explizites Festlegen von Nutzlastwerten

Im folgenden Beispiel fügen wir eine Nutzlasteigenschaft hinzu, die keinen automatisch generierten Wert verwendet:

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
}

Ein Beitrag kann jetzt auf die gleiche Weise wie zuvor kategorisiert werden, und die Verknüpfungsentität wird weiterhin automatisch erstellt. Auf diese Entität kann dann mithilfe eines der Mechanismen, die unter Zugriff auf nachverfolgte Entitäten beschrieben sind, zugegriffen werden. Der folgende Code verwendet z. B. DbSet<TEntity>.Find, um den Zugriff auf die Verknüpfungsentitätsinstanz zu verwenden:

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);

Nachdem die Verknüpfungsentität lokalisiert wurde, kann sie in diesem Beispiel auf die normale Art bearbeitet werden, um die Nutzlasteigenschaft TaggedBy vor dem Aufrufen von SaveChanges festzulegen.

Hinweis

Beachten Sie, dass hier ein Aufruf von ChangeTracker.DetectChanges() erforderlich ist, um EF Core die Möglichkeit zu geben, die Änderung der Navigationseigenschaft zu erkennen und die Verknüpfungsentitätsinstanz zu erstellen, bevor Find verwendet wird. Weitere Informationen finden Sie unter Änderungserkennung und Benachrichtigungen.

Alternativ kann die Verknüpfungsentität explizit erstellt werden, um einen Beitrag einem Tag zuzuordnen. Beispiel:

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);

Schließlich besteht eine weitere Möglichkeit zum Festlegen von Nutzlastdaten darin, Entitäten vor dem Aktualisieren der Datenbank entweder durch SaveChanges außer Kraft zu setzen oder das Ereignis DbContext.SavingChanges zum Verarbeiten von Entitäten zu verwenden. Beispiel:

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

    return base.SaveChanges();
}