Freigeben ü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 freundlich. Daher legen die meisten EF Core-Modelle auch "Navigationen" über die FK-Darstellung fest. Navigationsformulare bilden C#/.NET-Verweise zwischen Entitätsinstanzen, die 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 zur Modellierung 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 Change Tracking in EF Core .

Tipp

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

Beispielmodell

Das folgende Modell enthält vier Entitätstypen mit Beziehungen zwischen ihnen. Die Kommentare im Code geben an, welche Eigenschaften Fremdschlüssel, Primärschlüssel und Navigationseigenschaften 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 Kind. Sie enthält die FK-Eigenschaft Post.BlogId, deren Wert dem Blog.Id PK-Wert 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 Kind. Sie enthält die FK-Eigenschaft BlogAssets.BlogId, deren Wert dem Blog.Id PK-Wert 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 (n:n):
    • Viele-zu-viele-Beziehungen sind eine zusätzliche Ebene zu zwei Eins-zu-viele-Beziehungen. Viele-zu-viele-Beziehungen werden später 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 Navigationssammlung 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 ".

Beziehungsreparatur

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 "Beziehungsanpassung" bezeichnet.

Korrektur nach Abfrage

Das Fixup erfolgt erstmals, 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

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

Für jeden Blog erstellt EF Core zunächst eine Blog Instanz. Da dann jeder Beitrag aus der Datenbank geladen wird, wird die Post.Blog Referenznavigation so festgelegt, dass er auf den zugehörigen Blog verweist. Ebenso wird der Beitrag der Blog.Posts Sammlungsnavigation hinzugefügt. Dasselbe geschieht mit BlogAssets, außer in diesem Fall sind beide Navigationselemente Verweise. 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 Asset 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 wichtige Werte als auch Navigationen angezeigt. Navigationselemente werden mithilfe der Primärschlüsselwerte der zugehörigen Entitäten angezeigt. In der obigen Ausgabe wird beispielsweise angegeben, Posts: [{Id: 1}, {Id: 2}] dass die Blog.Posts Sammlungsnavigation zwei verwandte Beiträge mit Primärschlüsseln 1 bzw. 2 enthält. Ebenso weist die Blog: {Id: 1} Zeile für jeden Beitrag, der dem ersten Blog zugeordnet ist, darauf hin, dass die Post.Blog Navigation auf den Blog mit Primärschlüssel 1 verweist.

Anpassung für lokal nachverfolgte Einheiten

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

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

Die Blog.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 so korrigiert, dass sie auf die neu nachverfolgten BlogAsset Instanzen verweisen. 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 EF Core die Navigationen beim Nachverfolgen der Entitäten korrigierte, selbst wenn diese aus mehreren verschiedenen Abfragen kamen.

Hinweis

Fixup bewirkt niemals, dass mehr Daten aus der Datenbank zurückgegeben werden. Es stellt nur Verbindungen zu Entitäten her, die bereits im Ergebnis der Abfrage enthalten sind 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 erfolgen durch:

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

Hinzufügen oder Entfernen von Navigationsoptionen in Sammlungen

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

Tipp

Hier ist ein Aufruf ChangeTracker.DetectChanges() erforderlich, 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 Blog.Posts Navigation im .NET-Blog hat jetzt drei Beiträge (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Ebenso hat die Blog.Posts Navigation im Visual Studio-Blog nur einen Beitrag (Posts: [{Id: 4}]). Dies ist zu erwarten, da der Code diese Auflistungen explizit geändert hat.

Interessanter ist, auch wenn der Code die Post.Blog Navigation nicht explizit geändert hat, wurde es so behoben, 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 Post.Blog Referenznavigation 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 dem im vorherigen Beispiel. Dies liegt daran, dass EF Core die Änderung der Referenznavigation erkannt und dann die Sammlungsnavigationen und den FK-Wert angepasst hat, damit sie übereinstimmen.

Ändern von Beziehungen mithilfe von Fremdschlüsselwerten

Im vorherigen Abschnitt wurden Beziehungen durch Navigationen manipuliert, die es zulassen, dass Fremdschlüsselwerte automatisch aktualisiert werden. Dies 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 Post.BlogId Fremdschlüsselwert ändern:

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

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

Die Debugansicht nach dieser Änderung ist wieder identisch mit den beiden vorherigen Beispielen. Dies liegt daran, dass EF Core die Änderung des FK-Wertes erkannt und dann sowohl die Referenz- als auch die Sammlungsnavigationen angepasst hat, sodass sie miteinander ü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, passen Sie einfach eine einzelne Navigation an oder ändern Sie beide Navigationen. Ändern Sie bei Bedarf einfach die FK-Werte. Vermeiden Sie das Bearbeiten von Navigations- und FK-Werten.

Fixup 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 sie nachverfolgt. (Wenn die Entität normalerweise im Added Zustand ist. Sollte der Entitätstyp jedoch so konfiguriert sein, dass generierte Schlüssel verwendet werden, und der Primärschlüsselwert festgelegt sein, wird die Entität im Unchanged Status nachverfolgt.)
  • Wenn die Entität einem anderen Haupt- oder übergeordneten Element zugeordnet ist, wird diese Beziehung aufgehoben.
  • Die Entität wird der Hauptentität bzw. dem übergeordneten Element zugeordnet, das die Sammlungsführung besitzt.
  • Navigations- und Fremdschlüsselwerte sind für alle beteiligten Entitäten festgelegt.

Auf dieser Grundlage können wir sehen, dass, um einen Beitrag von einem Blog in einen anderen zu verschieben, wir ihn tatsächlich nicht aus der alten Sammlungsnavigation entfernen müssen, bevor wir ihn dem 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);

Nach:

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

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

Entfernen aus der Navigation einer Sammlung

Durch das Entfernen einer abhängigen/untergeordneten Entität aus der Sammlungsnavigation des Haupt-/übergeordneten Elements wird die Beziehung zu diesem Haupt-/ü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. Dies bedeutet, dass die abhängige/untergeordnete Person keiner Haupt- bzw. übergeordneten Bezugsperson / Elter mehr zugeordnet ist. Lassen Sie uns beispielsweise einen Blog und Beiträge laden und dann einen der Beiträge aus der Blog.Posts Sammlungsnavigation entfernen:

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

Nachdem diese Änderung vorgenommen wurde, zeigt die Debugansicht für die Änderungsnachverfolgung Folgendes:

  • Der Post.BlogId FK wurde auf NULL () festgelegt.BlogId: <null> FK Modified Originally 1
  • Die Post.Blog Referenznavigation wurde auf NULL festgelegt (Blog: <null>)
  • Der Beitrag wurde aus der Blog.Posts Sammlungsnavigation 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 Deletedgekennzeichnet ist. Sie ist so Modified gekennzeichnet, dass der FK-Wert in der Datenbank beim Aufrufen von SaveChanges auf NULL festgelegt wird.

Erforderliche Beziehungen

Das Festlegen des FK-Werts auf NULL ist für erforderliche Beziehungen nicht zulässig (und ist in der Regel 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 Beschrä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 Sie 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 so Deleted markiert, dass er aus der Datenbank gelöscht wird, wenn SaveChanges aufgerufen wird.
  • Die Post.Blog Referenznavigation wurde auf NULL (Blog: <null>) festgelegt.
  • Der Beitrag wurde aus der Blog.Posts Sammlungsnavigation (Posts: [{Id: 1}]) entfernt.
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 Post.BlogId Wert unverändert bleibt, da es für eine erforderliche Beziehung nicht auf NULL festgelegt 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 Zeitangaben für verwaiste Elemente und erneute Zuordnung zu einem übergeordneten Element

Standardmäßig werden Waisen markiert, Deleted sobald eine Änderung der Beziehung erkannt wird. Dieser Vorgang kann jedoch verzögert werden, bis SaveChanges tatsächlich aufgerufen wird. Dies kann hilfreich sein, um Entitäten zu vermeiden, die ohne übergeordnetes Element verbleiben, nachdem sie aus einem bisherigen übergeordneten System oder Prinzipal entfernt wurden, aber vor dem Aufrufen von SaveChanges einem neuen übergeordneten System oder Prinzipal zugeordnet 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);

await context.SaveChangesAsync();

Nach dem Entfernen des Beitrags aus der ersten Auflistung wird das Objekt nicht wie Deleted im vorherigen Beispiel markiert. Stattdessen verfolgt EF Core, dass die Beziehung getrennt wird, obwohl dies 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 behoben 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: []

SaveChanges, die an diesem Punkt aufgerufen werden, aktualisiert den Beitrag in der Datenbank, anstatt ihn zu löschen.

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 Verwaist nachverfolgt wird. Beispiel für diesen Code:

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

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 aufgelöst wird, konfigurieren Sie die Beziehung so, dass Löschweitergaben verwendet werden.

Das Löschen von verwaister Objekte sowie Löschweitergaben im Rahmen einer Kaskade kann jederzeit durch Aufrufen von ChangeTracker.CascadeChanges() erzwungen werden. Wenn Sie dies mit dem Festlegen des Löschwaisenzeitpunkts auf Never kombinieren, wird sichergestellt, dass Waiseneinträge niemals gelöscht werden, es sei denn, EF Core wird explizit angewiesen, dies zu tun.

Ändern einer Referenznavigation

Das Ändern der Referenznavigation einer Eins-zu-viele-Beziehung hat die gleiche Wirkung wie das Ändern der Sammlungsnavigation am anderen Ende der Beziehung. Das Festlegen der Referenznavigation eines abhängigen/untergeordneten Elements auf null entspricht dem Entfernen der Entität aus der Sammlungsnavigation des Hauptelements/übergeordneten Elements. Alle Fixup- 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 = 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();

Die Debugansicht vor dem Aufrufen von SaveChanges zeigt, dass die neuen Ressourcen die vorhandenen Ressourcen ersetzt haben, die 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, zeigt sich, dass die vorher zugeordnete Beziehung jetzt als BlogAssets markiert ist, da sie zum Waisen wird, wenn das neue Deleted seinen Platz einnimmt:

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

Der Zeitpunkt, zu dem Verwaiste als gelöscht markiert werden, kann auf dieselbe Weise wie bei Sammlungsnavigationen geändert werden und hat die gleichen Auswirkungen.

Löschen einer Entität

Optionale Beziehungen

Wenn eine Entität als Deleted gekennzeichnet ist, z. B. durch Aufrufen von DbContext.Remove, werden Verweise auf die gelöschte Entität aus den Navigationspfaden 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 = 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();

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.
  • 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 () und eine Nullverweisnavigation (BlogId: <null> FK Modified Originally 2) auf.Blog: <null>

Erforderliche Beziehungen

Das Fixupverhalten für erforderliche Beziehungen ist identisch mit dem für optionale Beziehungen, mit der Ausnahme, dass die abhängigen/untergeordneten Entitäten als Deleted gekennzeichnet sind, da sie nicht ohne ein Hauptelement/übergeordnetes Element existieren können und aus der Datenbank entfernt werden müssen, wenn SaveChanges aufgerufen wird, um eine Ausnahme wegen referenzieller Einschränkungen zu vermeiden. Dies wird als "Kaskadenlöschung" bezeichnet und ist das Standardverhalten in EF Core für erforderliche Beziehungen. Beispielsweise führt das Ausführen desselben Codes wie im vorherigen Beispiel, jedoch 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 Abhängige/Kinder 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 vermeidet, ein gelöschtes Diagramm von Entitäten komplett zu zerstören, 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 aufzuheben, als in EF6 der Fall war, in dem das Diagramm zerkleinert wurde.

Kaskadierende Löschung und Neuzuordnung von Eltern-Kind-Beziehungen

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

Löschweitergaben sowie das Löschen von verwaisten Objekten können jederzeit durch das Aufrufen von ChangeTracker.CascadeChanges() erzwungen werden. Indem Sie den Zeitpunkt der Löschweitergabe auf Never festlegen, stellen Sie sicher, dass Löschweitergaben niemals ausgeführt werden, es sei denn, EF Core wird explizit angewiesen, dies zu tun.

Tipp

Kaskadenlöschung und die Löschung von Waisenobjekten sind eng miteinander verknüpft. Beide führen zum Löschen von abhängigen/untergeordneten Entitäten, wenn die Beziehung zu ihrem notwendigen Haupt-/Elternelement aufgelöst wird. Bei einem kaskadierenden Löschen erfolgt diese Trennung, da das Prinzipal/das übergeordnete Element selbst gelöscht wird. Für Waisen existiert die Haupt-/übergeordnete Entität weiterhin, ist jedoch nicht mehr mit den abhängigen/untergeordneten Entitäten verbunden.

M:n-Beziehungen

Viele-zu-viele-Beziehungen in EF Core werden mithilfe einer Verknüpfungsentität implementiert. Jede Seite der m:n-Beziehung ist über eine eins-zu-viele-Verknüpfung mit dieser Verknüpfungsentität verbunden. Diese Verknüpfungsentität kann klar 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.

Funktionsweise von Viele-zu-Viele-Beziehungen

Betrachten Sie dieses EF Core-Modell, das eine Viele-zu-Viele-Beziehung zwischen Beiträgen und Tags mithilfe eines explizit definierten Join-Entitä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 PostTag Verknüpfungsentitätstyp zwei Fremdschlüsseleigenschaften enthält. In diesem Modell muss für einen Beitrag, der mit einem Tag verknüpft ist, eine PostTag-Verknüpfungsentität vorhanden sein, bei der der PostTag.PostId Fremdschlüsselwert mit dem Post.Id Primärschlüsselwert übereinstimmt und wo der PostTag.TagId Fremdschlüsselwert mit dem Tag.Id Primärschlüsselwert übereinstimmt. Beispiel:

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

Wenn Sie sich nach der Ausführung dieses Codes die Debugansicht der Änderungsverfolgung ansehen, zeigt sich, dass der Beitrag und das Tag durch die neue PostTag Verknüpfungsentität miteinander verbunden sind.

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 sowie die Referenznavigationen auf PostTag behoben wurden. Diese Beziehungen können durch Navigationen anstelle von FK Werten gesteuert werden, genau wie in allen vorherigen Beispielen. Beispielsweise kann der obige Code geändert werden, um die Beziehung hinzuzufügen, indem die Referenznavigationen für 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, die die Verknüpfungsentität "überspringen". So können dem obigen Modell beispielsweise zwei Übersprungnavigationen hinzugefügt werden, eine von "Post" in "Tags" und die andere von "Tag" zu "Posts".

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 Viele-zu-viele-Beziehung ist die folgende Konfiguration erforderlich, um sicherzustellen, dass sowohl übersprungene Navigationen als auch normale Navigationen für dieselbe Viele-zu-viele-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));
}

Sehen Sie Beziehungen für weitere Informationen zum Zuordnen von m:n-Beziehungen.

Skip-Navigationsleisten sehen aus und 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 Überspringungsnavigation:

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

Beachten Sie, dass dieser Code die Verknüpfungsentität nicht verwendet. Stattdessen wird einer Navigationssammlung eine Entität hinzugefügt, und zwar auf dieselbe Weise, wie es bei einer Eins-zu-viele-Beziehung der Fall wäre. Die resultierende Debugansicht ist im Wesentlichen gleich 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 eine Instanz der PostTag Verknüpfungsentität automatisch erstellt wurde, wobei die FK-Werte auf die PK-Werte des Tags und des Beitrags festgelegt sind, die aktuell zugeordnet sind. Alle normalen Referenz- und Sammlungsnavigationen wurden so behoben, dass sie diesen FK-Werten entsprechen. Da dieses Modell das Überspringen von Navigationen unterstützt, wurden diese ebenfalls angepasst. Obwohl wir das Tag zum Post.Tags Navigationsüberspringen hinzugefügt haben, wurde das Tag.Posts umgekehrte Navigationsüberspringen auf der anderen Seite dieser Beziehung ebenfalls so angepasst, dass der zugeordnete Beitrag enthalten ist.

Es lohnt sich zu beachten, dass die zugrunde liegenden Viele-zu-Viele-Beziehungen auch dann direkt bearbeitet werden können, wenn Skipnavigationen darüber gelegt wurden. Beispielsweise könnte das Tag und der Beitrag wie zuvor zugeordnet werden, bevor wir Navigationen überspringen:

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 Navigation überspringen

Im vorherigen Abschnitt haben wir zusätzlich zu der vollständigen Definition der beiden zugrunde liegenden Eins-zu-viele-Beziehungen Navigationsmöglichkeiten zum Überspringen hinzugefügt. Dies ist nützlich, um zu veranschaulichen, was mit FK-Werten geschieht, aber häufig unnötig. Stattdessen kann die viele-zu-viele-Beziehung nur mithilfe von Übersprungnavigationen definiert werden. Auf diese Weise wird die Viele-zu-Viele-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 Tag.Posts Navigation übersprungen einen Beitrag hinzufügen (oder alternativ ein Tag zur Überspringungsnavigation Post.Tags hinzufügen):

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

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, um die Verknüpfungsentität darzustellen. Diese Verknüpfungsentität enthält sowohl PostsId als auch TagsId Fremdschlüsseleigenschaften, die so festgelegt wurden, dass sie den PK-Werten des zugeordneten Beitrags und der zugeordneten 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 finden Sie unter Dictionary<string, object>".

Von Bedeutung

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. Hängen Sie nicht davon ab, dass der Verknüpfungstyp Dictionary<string, object> wird, 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 n:n-Beziehung erforderlich sind. Keine 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 Zuweisung einer "Payload" an die Verknüpfungsentität bezeichnet. Nehmen wir beispielsweise die Eigenschaft TaggedOn zur Verknüpfungsentität PostTag hinzu:

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 gängigste Methode 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, wenn jede 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 = 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);

Wenn Sie die Debugansicht der Änderungsverfolgung nach dem Aufrufen von SaveChanges betrachten, zeigt sich, 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 zugegriffen werden, die unter "Zugriff auf nachverfolgte Entitäten" beschrieben sind. Zum Beispiel verwendet der folgende Code DbSet<TEntity>.Find, um auf die Instanz der Join-Entität zuzugreifen.

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

Nachdem sich die Verknüpfungsentität befindet, kann sie in diesem Beispiel normal bearbeitet werden, um die TaggedBy Nutzlasteigenschaft 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 Join-Entitätsinstanz zu erstellen, bevor sie von Find verwendet wird. Weitere Informationen finden Sie unter Änderungserkennung und Benachrichtigungen .

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

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

Schließlich besteht eine andere Möglichkeit zum Festlegen von Nutzlastdaten darin, entweder SaveChanges zu überschreiben oder das DbContext.SavingChanges-Ereignis zu verwenden, um Entitäten vor dem Aktualisieren der Datenbank zu verarbeiten. Beispiel:

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