Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Viele-zu-Viele-Beziehungen werden genutzt, wenn eine beliebige Anzahl von Entitäten eines Entitätstyps mit einer beliebigen Anzahl von Entitäten desselben oder eines anderen Entitätstyps verbunden ist. Beispielsweise kann ein Post
viele Tags
zugeordnet haben, und jeder Tag
kann wiederum einer beliebigen Anzahl von Posts
zugeordnet werden.
Grundlegendes zu M:N-Beziehungen
Viele-zu-Viele-Beziehungen unterscheiden sich von Eins-zu-Viele-- und Eins-zu-Eins--Beziehungen darin, dass sie nicht auf einfache Weise nur mithilfe eines Fremdschlüssels dargestellt werden können. Stattdessen wird ein zusätzlicher Entitätstyp benötigt, um die beiden Seiten der Beziehung zu "verbinden". Dies wird als "Verknüpfungsentitätstyp" bezeichnet und einer "Join-Tabelle" in einer relationalen Datenbank zugeordnet. Die Entitäten dieses Verknüpfungsentitätstyps enthalten Paare von Fremdschlüsselwerten, wobei eines der einzelnen Paare auf eine Entität auf einer Seite der Beziehung zeigt und die andere auf eine Entität auf der anderen Seite der Beziehung verweist. Jede Verknüpfungsentität und daher jede Zeile in der Verknüpfungstabelle stellt daher eine Zuordnung zwischen den Entitätstypen in der Beziehung dar.
EF Core kann den Joinentitätstyp ausblenden und im Hintergrund verwalten. Auf diese Weise können die Navigationen einer M:N-Beziehung auf natürliche Weise verwendet werden, indem Sie bei Bedarf Entitäten von jeder Seite hinzufügen oder entfernen. Es ist jedoch hilfreich zu verstehen, was hinter den Kulissen geschieht, damit ihr gesamtes Verhalten und insbesondere die Zuordnung zu einer relationalen Datenbank sinnvoll ist. Beginnen wir mit der Einrichtung eines relationalen Datenbankschemas, um eine viele-zu-viele Beziehung zwischen Beiträgen und Tags darzustellen.
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
In diesem Schema ist PostTag
die Verknüpfungstabelle. Sie enthält zwei Spalten: PostsId
, bei dem es sich um einen Fremdschlüssel für den Primärschlüssel der Posts
Tabelle handelt, und TagsId
, bei dem es sich um einen Fremdschlüssel für den Primärschlüssel der Tags
Tabelle handelt. Jede Zeile in dieser Tabelle stellt daher eine Zuordnung zwischen einem Post
und einem Tag
dar.
Eine vereinfachte Zuordnung für dieses Schema in EF Core besteht aus drei Entitätstypen für jede Tabelle. Wenn jeder dieser Entitätstypen durch eine .NET-Klasse dargestellt wird, sehen diese Klassen möglicherweise wie folgt aus:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostsId { get; set; }
public int TagsId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Beachten Sie, dass es in dieser Zuordnung keine M:N-Beziehung gibt, sondern zwei 1:N-Beziehungen, eine für jeden der in der Jointabelle definierten Fremdschlüssel. Dies ist keine unvernünftige Art, diese Tabellen zuzuordnen, entspricht aber nicht der Absicht der Jointabelle, die eine einzelne M:N-Beziehung und nicht zwei 1:N-Beziehungen darstellen soll.
EF ermöglicht eine natürlichere Zuordnung durch die Einführung von zwei Sammlungsnavigationen, eine auf Post
, die die zugehörigen Tags
enthält, und eine umgekehrte auf Tag
, die die zugehörigen Posts
enthält. Beispiel:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = [];
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int PostsId { get; set; }
public int TagsId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Tipp
Diese neuen Navigationen werden als „überspringende Navigationen“ bezeichnet, da sie die Joinentität überspringen und direkten Zugriff auf die andere Seite der M:N-Beziehung bieten.
Wie im vorstehenden Beispiel gezeigt, kann eine m:n-Beziehung auf diese Weise zugeordnet werden, d. h. mit einer .NET-Klasse für die Joinentität und mit beiden Navigationen für die beiden 1:n-Beziehungen und den überspringenden Navigationen, die für die Entitätstypen verfügbar gemacht werden. EF kann die Joinentität jedoch transparent verwalten, ohne dass eine .NET-Klasse dafür definiert ist und ohne Navigationen für die beiden 1:N-Beziehungen. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
In der Tat werden EF Modellierungskonventionen standardmäßig die Typen Post
und Tag
, die hier gezeigt werden, den drei Tabellen im Datenbankschema oben in diesem Abschnitt zuordnen. Diese Zuordnung ohne explizite Verwendung des Jointyps ist das, was typischerweise mit dem Begriff „M:N“ gemeint ist.
Beispiele
Die folgenden Abschnitte enthalten Beispiele für viele-zu-viele-Beziehungen, einschließlich der Konfiguration, die für jede Zuordnung erforderlich ist.
Tipp
Der Code für alle folgenden Beispiele finden Sie in ManyToMany.cs.
Einfache M:N-Beziehung
Im einfachsten Fall einer M:N-Beziehung verfügen die Entitäten auf beiden Seiten der Beziehung über eine Sammlungsnavigation. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Diese Beziehung wird gemäß Konvention zugeordnet. Obwohl es nicht erforderlich ist, wird unten eine entsprechende explizite Konfiguration für diese Beziehung als Lerntool angezeigt:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts);
}
Auch bei dieser expliziten Konfiguration sind viele Aspekte der Beziehung nach wie vor konventionskonfiguriert. Eine umfassendere explizite Konfiguration, die wiederum für Lernzwecke gilt:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
"PostTag",
r => r.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
l => l.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
j => j.HasKey("PostsId", "TagsId"));
}
Wichtig
Versuchen Sie nicht, alles auch dann vollständig zu konfigurieren, wenn sie nicht benötigt wird. Wie oben zu sehen ist, wird der Code schnell kompliziert und es ist leicht, einen Fehler zu machen. Und auch im obigen Beispiel gibt es viele Dinge im Modell, die nach wie vor konventionskonfiguriert sind. Es ist nicht realistisch zu erwarten, dass in einem EF-Modell immer alles explizit konfiguriert werden kann.
Unabhängig davon, ob die Beziehung konventionsgemäß erstellt oder eine der angezeigten expliziten Konfigurationen verwendet wird, lautet das resultierende zugeordnete Schema (mithilfe von SQLite):
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tipp
Wenn Sie einen Database-First-Flow verwenden, um ein DbContext-Gerüst aus einer bestehenden Datenbank zu erstellen, sucht EF Core 6 und höher nach diesem Muster im Datenbankschema und erstellt ein M:N-Beziehungsgerüst, wie in diesem Dokument beschrieben. Dieses Verhalten kann mithilfe einer benutzerdefinierten T4-Vorlagegeändert werden. Weitere Optionen finden Sie unter Für M:N-Beziehungen ohne zugeordnete Joinentitäten wird jetzt ein Gerüst verwendet.
Wichtig
Derzeit verwendet EF Core Dictionary<string, object>
, um Verknüpfungsentitätsinstanzen darzustellen, für die keine .NET-Klasse konfiguriert wurde. Um die Leistung zu verbessern, kann jedoch ein anderer Typ in einer zukünftigen EF Core-Version verwendet werden. Hängen Sie nicht davon ab, dass der Verknüpfungstyp Dictionary<string, object>
wird, es sei denn, dies wurde explizit konfiguriert.
M:N-Beziehung mit benannter Jointabelle
Im vorherigen Beispiel wurde die Verknüpfungstabelle nach Konvention PostTag
benannt. Sie kann mit UsingEntity
einen expliziten Namen erhalten. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity("PostsToTagsJoinTable");
}
Alles andere an der Zuordnung bleibt gleich, nur der Name der Jointabelle ändert sich:
CREATE TABLE "PostsToTagsJoinTable" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung mit Fremdschlüsselnamen der Jointabelle
Aufbauend auf dem vorherigen Beispiel können auch die Namen der Fremdschlüsselspalten in der Verknüpfungstabelle geändert werden. Es gibt zwei Möglichkeiten, dies zu tun. Der erste Schritt besteht darin, die Fremdschlüssel-Eigenschaftsnamen für die Verknüpfungsentität explizit anzugeben. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
r => r.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
l => l.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}
Die zweite Möglichkeit besteht darin, die Eigenschaften mit ihren Nachkonventionsnamen zu belassen, diese Eigenschaften dann jedoch unterschiedlichen Spaltennamen zuzuordnen. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
j =>
{
j.Property("PostsId").HasColumnName("PostForeignKey");
j.Property("TagsId").HasColumnName("TagForeignKey");
});
}
In beiden Fällen bleibt die Zuordnung gleich, nur die Namen der Fremdschlüsselspalten werden geändert:
CREATE TABLE "PostTag" (
"PostForeignKey" INTEGER NOT NULL,
"TagForeignKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tipp
Obwohl hier nicht gezeigt, können die beiden vorherigen Beispiele kombiniert werden, um den Namen der Jointabelle und die Spaltennamen der Fremdschlüssel zu ändern.
M:N-Beziehung mit Klasse für die Joinentität
In den bisherigen Beispielen wurde die Jointabelle automatisch zu einer Entität vom Typ freigegeben zugeordnet. Dadurch wird die Notwendigkeit entfernt, eine dedizierte Klasse für den Entitätstyp zu erstellen. Es kann jedoch hilfreich sein, eine solche Klasse zu verwenden, damit sie leicht referenziert werden kann, insbesondere, wenn Navigationen oder nutzlasten (eine "Nutzlast" ist alle zusätzlichen Daten in der Verknüpfungstabelle. Beispielsweise wird der Zeitstempel, in dem ein Eintrag in der Verknüpfungstabelle erstellt wird.) werden der Klasse hinzugefügt, wie in späteren Beispielen unten gezeigt. Erstellen Sie dazu zunächst zusätzlich zu den vorhandenen Typen für PostTag
und Post
einen Typ Tag
für die Verknüpfungsentität:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
}
Tipp
Die Klasse kann einen beliebigen Namen haben, aber es ist üblich, die Namen der Typen am Ende der Beziehung zu kombinieren.
Jetzt kann die UsingEntity
-Methode verwendet werden, um dies als Verknüpfungsentitätstyp für die Beziehung zu konfigurieren. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
PostId
und TagId
werden automatisch als Fremdschlüssel übernommen und als zusammengesetzter Primärschlüssel für den Joinentitätstyp konfiguriert. Die für die Fremdschlüssel zu verwendenden Eigenschaften können explizit für Fälle konfiguriert werden, in denen sie nicht mit der EF-Konvention übereinstimmen. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
r => r.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
l => l.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}
Das zugeordnete Datenbankschema für die Verknüpfungstabelle in diesem Beispiel entspricht strukturell den vorherigen Beispielen, jedoch mit einigen unterschiedlichen Spaltennamen:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung mit Navigationen zur Joinentität
Im anschluss an das vorherige Beispiel, da es nun eine Klasse gibt, die die Verknüpfungsentität darstellt, kann es leicht Navigationselemente hinzufügen, die auf diese Klasse verweisen. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
}
Wichtig
Wie in diesem Beispiel gezeigt, können Navigationen zum Joinentitätstyp zusätzlich zu den überspringenden Navigationen zwischen den beiden Enden der M:N-Beziehung verwendet werden. Dies bedeutet, dass die überspringenden Navigationen verwendet werden können, um auf natürliche Weise mit der M:N-Beziehung zu interagieren, während die Navigationen zum Joinentitätstyp verwendet werden können, wenn eine größere Kontrolle über die Joinentitäten selbst erforderlich ist. In gewisser Weise bietet diese Zuordnung das Beste aus beiden Welten zwischen einer einfachen Viele-zu-Viele-Zuordnung und einer Zuordnung, die expliziter mit dem Datenbankschema übereinstimmt.
In dem Aufruf von UsingEntity
muss nichts geändert werden, da die Navigationen zur Joinentität gemäß Konvention übernommen werden. Daher ist die Konfiguration für dieses Beispiel identisch mit dem letzten Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Die Navigationen können explizit für Fälle konfiguriert werden, in denen sie nicht durch Konventionen bestimmt werden können. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
r => r.HasOne<Tag>().WithMany(e => e.PostTags),
l => l.HasOne<Post>().WithMany(e => e.PostTags));
}
Das zugeordnete Datenbankschema ist nicht betroffen, indem Navigationselemente in das Modell eingeschlossen werden:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung mit Navigationen zu und von der Joinentität
Im vorigen Beispiel wurden Navigationen zum Joinentitätstyp von den Entitätstypen an beiden Enden der M:N-Beziehung hinzugefügt. Navigationsoptionen können auch in die andere Richtung oder in beide Richtungen hinzugefügt werden. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
In dem Aufruf von UsingEntity
muss nichts geändert werden, da die Navigationen zur Joinentität gemäß Konvention übernommen werden. Daher ist die Konfiguration für dieses Beispiel identisch mit dem letzten Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Die Navigationen können explizit für Fälle konfiguriert werden, in denen sie nicht durch Konventionen bestimmt werden können. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
l => l.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}
Das zugeordnete Datenbankschema ist nicht betroffen, indem Navigationselemente in das Modell eingeschlossen werden:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung mit Navigationen und geänderten Fremdschlüsseln
Das vorherige Beispiel zeigte eine M:N-Beziehung mit Navigationen zu und von dem Joinentitätstyp. Dieses Beispiel ist identisch, mit der Ausnahme, dass die verwendeten Fremdschlüsseleigenschaften ebenfalls geändert werden. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostForeignKey { get; set; }
public int TagForeignKey { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Auch hier wird die UsingEntity
-Methode verwendet, um folgendes zu konfigurieren:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
l => l.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}
Das zugeordnete Datenbankschema lautet jetzt:
CREATE TABLE "PostTag" (
"PostForeignKey" INTEGER NOT NULL,
"TagForeignKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Unidirektionale M:N-Beziehung
Es ist nicht notwendig, auf beiden Seiten der M:N-Beziehung eine Navigation einzubeziehen. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
}
EF muss konfiguriert werden, um zu wissen, dass es sich um eine M:N-Beziehung und nicht um eine 1:N-Beziehung handelt. Dies geschieht mithilfe von HasMany
und WithMany
, wobei jedoch kein Argument auf der Seite ohne Navigation übergeben wird. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany();
}
Das Entfernen der Navigation wirkt sich nicht auf das Datenbankschema aus:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung und Jointabelle mit Nutzdaten
In den Beispielen bisher wurde die Verknüpfungstabelle nur verwendet, um die Fremdschlüsselpaare zu speichern, die jede Zuordnung darstellen. Es kann jedoch auch verwendet werden, um Informationen über die Zuordnung zu speichern, z. B. die Zeit, zu der sie erstellt wurde. In solchen Fällen empfiehlt es sich, einen Typ für die Verknüpfungsentität zu definieren und diesem Typ die Eigenschaften "Zuordnungsnutzlast" hinzuzufügen. Es ist auch üblich, zusätzlich zu den „überspringenden Navigationen“, die für die M:N-Beziehung verwendet werden, Navigationen für die Joinentität zu erstellen. Diese zusätzlichen Navigationen ermöglichen es der Verknüpfungseinheit, einfach im Code referenziert zu werden, wodurch das Lesen und/oder Ändern der Nutzlastdaten erleichtert wird. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public DateTime CreatedOn { get; set; }
}
Es ist auch üblich, generierte Werte für Nutzdateneigenschaften zu verwenden, z. B. einen Datenbankzeitstempel, der automatisch festgelegt wird, wenn die Zuordnungszeile eingefügt wird. Dies erfordert eine minimale Konfiguration. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Das Ergebnis wird einem Entitätstypschema zugeordnet, bei dem ein Zeitstempel automatisch festgelegt wird, wenn eine Zeile eingefügt wird:
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Tipp
Die hier gezeigte SQL ist für SQLite. Verwenden Sie in SQL Server/Azure SQL .HasDefaultValueSql("GETUTCDATE()")
und für TEXT
lesen Sie datetime
.
Benutzerdefinierter Entitätstyp „Freigegeben“ als Joinentität
Im vorherigen Beispiel wurde der Typ PostTag
als Verknüpfungsentitätstyp verwendet. Dieser Typ ist spezifisch für die Posts-Tags-Beziehung. Wenn Sie jedoch mehrere Verknüpfungstabellen mit derselben Form haben, kann derselbe CLR-Typ für alle verwendet werden. Stellen Sie sich z. B. vor, dass alle unsere Jointabellen eine CreatedOn
-Spalte aufweisen. Wir können diese mithilfe der JoinType
-Klasse als Entitätstyp „Freigegeben“ zuordnen:
public class JoinType
{
public int Id1 { get; set; }
public int Id2 { get; set; }
public DateTime CreatedOn { get; set; }
}
Auf diesen Typ kann dann durch mehrere unterschiedliche M:N-Beziehungen als Joinentitätstyp verwiesen werden. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
public List<JoinType> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
public List<JoinType> PostTags { get; } = [];
}
public class Blog
{
public int Id { get; set; }
public List<Author> Authors { get; } = [];
public List<JoinType> BlogAuthors { get; } = [];
}
public class Author
{
public int Id { get; set; }
public List<Blog> Blogs { get; } = [];
public List<JoinType> BlogAuthors { get; } = [];
}
Und diese Beziehungen können dann entsprechend konfiguriert werden, um den Verknüpfungstyp einer anderen Tabelle für jede Beziehung zuzuordnen:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<JoinType>(
"PostTag",
r => r.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
l => l.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
modelBuilder.Entity<Blog>()
.HasMany(e => e.Authors)
.WithMany(e => e.Blogs)
.UsingEntity<JoinType>(
"BlogAuthor",
r => r.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
l => l.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Dies führt zu den folgenden Tabellen im Datenbankschema:
CREATE TABLE "BlogAuthor" (
"Id1" INTEGER NOT NULL,
"Id2" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);
CREATE TABLE "PostTag" (
"Id1" INTEGER NOT NULL,
"Id2" INTEGER NOT NULL,
"CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung mit Alternativschlüsseln
Bisher haben alle Beispiele gezeigt, wie die Fremdschlüssel im Verknüpfungsentitätstyp auf die Primärschlüssel der Entitätstypen auf beiden Seiten der Beziehung beschränkt sind. Jeder Fremdschlüssel oder beides kann stattdessen auf einen alternativen Schlüssel beschränkt werden. Betrachten Sie beispielsweise dieses Modell, bei demTag
und Post
alternative Schlüsseleigenschaften aufweisen:
public class Post
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Post> Posts { get; } = [];
}
Die Konfiguration für dieses Modell lautet:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
r => r.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
l => l.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}
Und das resultierende Datenbankschema, zur Übersichtlichkeit, einschließlich der Tabellen mit den alternativen Schlüsseln:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "PostTag" (
"PostsAlternateKey" INTEGER NOT NULL,
"TagsAlternateKey" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);
Die Konfiguration für die Verwendung alternativer Schlüssel unterscheidet sich geringfügig, wenn der Verknüpfungsentitätstyp durch einen .NET-Typ dargestellt wird. Beispiel:
public class Post
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Tag> Tags { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public int AlternateKey { get; set; }
public List<Post> Posts { get; } = [];
public List<PostTag> PostTags { get; } = [];
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Die Konfiguration kann jetzt die generische UsingEntity<>
-Methode verwenden:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>(
r => r.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
l => l.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}
Und das resultierende Schema lautet:
CREATE TABLE "Posts" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
"AlternateKey" INTEGER NOT NULL,
CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));
CREATE TABLE "PostTag" (
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);
M:N-Beziehung und Jointabelle mit separatem Primärschlüssel
Bisher verfügt der Verknüpfungsentitätstyp in allen Beispielen über einen Primärschlüssel, der aus den beiden Fremdschlüsseleigenschaften besteht. Dies liegt daran, dass jede Kombination von Werten für diese Eigenschaften höchstens einmal auftreten kann. Diese Eigenschaften bilden daher einen natürlichen Primärschlüssel.
Hinweis
EF Core unterstützt keine doppelten Entitäten in einer Sammlungsnavigation.
Wenn Sie das Datenbankschema steuern, gibt es keinen Grund für die Verknüpfungstabelle, eine zusätzliche Primärschlüsselspalte zu haben. Es ist jedoch möglich, dass eine vorhandene Verknüpfungstabelle möglicherweise eine Primärschlüsselspalte definiert hat. EF kann mit einer gewissen Konfiguration immer noch entsprechend zugeordnet werden.
Dies ist vielleicht am einfachsten, indem eine Klasse erstellt wird, die die Verknüpfungsentität darstellt. Beispiel:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
public class PostTag
{
public int Id { get; set; }
public int PostId { get; set; }
public int TagId { get; set; }
}
Diese PostTag.Id
-Eigenschaft wird nun standardmäßig als Primärschlüssel erkannt, sodass nur die Konfiguration eines Aufrufs von UsingEntity
für den PostTag
-Typ erforderlich ist.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity<PostTag>();
}
Und das resultierende Schema für die Verknüpfungstabelle lautet:
CREATE TABLE "PostTag" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
"PostId" INTEGER NOT NULL,
"TagId" INTEGER NOT NULL,
CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
Ein Primärschlüssel kann auch der Verknüpfungsentität hinzugefügt werden, ohne eine Klasse dafür zu definieren. Beispielsweise mit ausschließlich Post
- und Tag
-Typen:
public class Post
{
public int Id { get; set; }
public List<Tag> Tags { get; } = [];
}
public class Tag
{
public int Id { get; set; }
public List<Post> Posts { get; } = [];
}
Der Schlüssel kann mit dieser Konfiguration hinzugefügt werden:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
j =>
{
j.IndexerProperty<int>("Id");
j.HasKey("Id");
});
}
Dies führt zu einer Verknüpfungstabelle mit einer separaten Primärschlüsselspalte:
CREATE TABLE "PostTag" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);
M:N-Beziehung ohne kaskadierendes Delete
In allen oben gezeigten Beispielen werden die Fremdschlüssel, die zwischen der Jointabelle und den beiden Seiten der M:N-Beziehung erstellt wurden, mit kaskadierendem Delete-Verhalten erstellt. Dies ist sehr nützlich, da dies bedeutet, dass die Zeilen in der Verknüpfungstabelle für diese Entität automatisch gelöscht werden, wenn eine Entität auf beiden Seiten der Beziehung gelöscht wird. Oder anders ausgedrückt, wenn eine Entität nicht mehr vorhanden ist, sind ihre Beziehungen zu anderen Entitäten auch nicht mehr vorhanden.
Es ist schwierig, sich vorzustellen, wenn es nützlich ist, dieses Verhalten zu ändern, aber es kann bei Bedarf durchgeführt werden. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(e => e.Tags)
.WithMany(e => e.Posts)
.UsingEntity(
r => r.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
l => l.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}
Das Datenbankschema für die Jointabelle wendet ein eingeschränktes Löschverhalten auf die Fremdschlüsseleinschränkung an:
CREATE TABLE "PostTag" (
"PostsId" INTEGER NOT NULL,
"TagsId" INTEGER NOT NULL,
CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);
Auf sich selbst verweisende M:N-Beziehung
Derselbe Entitätstyp kann an beiden Enden einer viele-zu-viele-Beziehung verwendet werden; dies wird als "selbstverweisende" Beziehung bezeichnet. Beispiel:
public class Person
{
public int Id { get; set; }
public List<Person> Parents { get; } = [];
public List<Person> Children { get; } = [];
}
Dies entspricht einer Verknüpfungstabelle namens PersonPerson
, wobei beide Fremdschlüssel auf die Tabelle People
verweisen.
CREATE TABLE "PersonPerson" (
"ChildrenId" INTEGER NOT NULL,
"ParentsId" INTEGER NOT NULL,
CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);
Symmetrische, auf sich selbst verweisende M:N-Beziehung
Manchmal ist eine Viele-zu-Viele-Beziehung von Natur aus symmetrisch. Das heißt, wenn Entität A mit Entität B verknüpft ist, ist entität B auch mit Entität A verknüpft. Dies wird natürlich mit einer einzigen Navigation modelliert. Stellen Sie sich zum Beispiel den Fall vor, in dem Person A mit Person B befreundet ist und Person B mit Person A befreundet ist.
public class Person
{
public int Id { get; set; }
public List<Person> Friends { get; } = [];
}
Leider ist dies nicht einfach zuzuordnen. Die gleiche Navigation kann nicht für beide Enden der Beziehung verwendet werden. Die beste Möglichkeit besteht darin, sie als unidirektionale M:N-Beziehung zuzuordnen. Beispiel:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasMany(e => e.Friends)
.WithMany();
}
Um jedoch sicherzustellen, dass zwei Personen miteinander verbunden sind, muss jede Person manuell zur Friends
Sammlung der anderen Person hinzugefügt werden. Beispiel:
ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);
Direkte Verwendung der Jointabelle
Alle obigen Beispiele verwenden die M:N-Zuordnungsmuster von EF Core. Es ist jedoch auch möglich, eine Jointabelle einem normalen Entitätstyp zuzuordnen und nur die beiden 1:N-Beziehungen für alle Vorgänge zu verwenden.
Diese Entitätstypen stellen z. B. die Zuordnung von zwei normalen Tabellen und einer Jointabelle dar, ohne M:N-Beziehungen zu verwenden:
public class Post
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = new();
}
public class Tag
{
public int Id { get; set; }
public List<PostTag> PostTags { get; } = new();
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public Post Post { get; set; } = null!;
public Tag Tag { get; set; } = null!;
}
Dies erfordert keine spezielle Zuordnung, da es sich hierbei um normale Entitätstypen mit normalen eins-zu-viele Beziehungen handelt.
Weitere Ressourcen
- .NET Data Community Standup-Sitzung, mit detaillierten Einblicken in M:N-Beziehungen und die zugrunde liegende Infrastruktur.