Dela via


Många-till-många-relationer

Många-till-många-relationer används när valfritt antal entiteter av en entitetstyp är associerade med valfritt antal entiteter av samma eller någon annan entitetstyp. En Post kan till exempel ha många associerade Tagsoch varje Tag kan i sin tur associeras med valfritt antal Posts.

Förstå många-till-många-relationer

Många-till-många-relationer skiljer sig från en-till-många-- och en-till-en--relationer eftersom de inte kan representeras på ett enkelt sätt med bara en främmande nyckel. I stället behövs en ytterligare entitetstyp för att "koppla" de två sidorna av relationen. Detta kallas "kopplingsentitetstyp" och motsvarar en "kopplingstabell" i en relationsdatabas. Entiteterna för den här kopplingsentitetstypen innehåller par med sekundärnyckelvärden, där ett av varje par pekar på en entitet på ena sidan av relationen och den andra pekar på en entitet på andra sidan relationen. Varje kopplingsentitet, och därmed varje rad i kopplingstabellen, representerar därför en association mellan entitetstyperna i relationen.

EF Core kan dölja anslutningsentitetstypen och hantera den i bakgrunden. Detta gör att navigeringen i en många-till-många-relation kan användas på ett naturligt sätt, vilket lägger till eller tar bort entiteter från varje sida efter behov. Det är dock användbart att förstå vad som händer bakom kulisserna så att deras övergripande beteende, och i synnerhet mappningen till en relationsdatabas, är meningsfullt. Vi börjar med en relationsdatabasschemakonfiguration för att representera en många-till-många-relation mellan inlägg och taggar:

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

I det här schemat är PostTag kopplingstabellen. Den innehåller två kolumner: PostsId, som är en sekundärnyckel till den primära nyckeln i tabellen Posts och TagsId, som är en sekundärnyckel till primärnyckeln för Tags-tabellen. Varje rad i den här tabellen representerar därför en association mellan en Post och en Tag.

En förenklad mappning för det här schemat i EF Core består av tre entitetstyper – en för varje tabell. Om var och en av dessa entitetstyper representeras av en .NET-klass kan dessa klasser se ut så här:

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

Observera att det i den här mappningen inte finns någon många-till-många-relation, utan snarare två en-till-många-relationer, en för varje extern nyckel som definierats i kopplingstabellen. Detta är inte ett orimligt sätt att mappa dessa tabeller, men återspeglar inte avsikten med kopplingstabellen, som är att representera en enda många-till-många-relation i stället för två en-till-många-relationer.

EF möjliggör en mer naturlig mappning genom introduktionen av två samlingsnavigeringar, en på Post som innehåller dess relaterade Tagsoch en invers på Tag som innehåller dess relaterade Posts. Till exempel:

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

Tips

Dessa nya navigeringar kallas "genvägsnavigeringar", eftersom de genar förbi kopplingsentiteten för att ge direkt åtkomst till den andra sidan av en många-till-många-relation.

Som visas i exemplet ovan kan en många-till-många-relation mappas på det här sättet; det vill säga med en .NET-klass för kopplingsentiteten och med navigeringarna för de två en-till-många-relationerna och hoppa över navigeringar som exponeras på entitetstyperna. EF kan dock hantera kopplingsentiteten på ett transparent sätt, utan en .NET-klass som definierats för den, och utan navigeringar för de två en-till-många-relationerna. Till exempel:

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; } = [];
}

EF modellbyggnadskonventionerna kommer som standard att mappa Post- och Tag-typerna som visas här till de tre tabellerna i databasschemat överst i det här avsnittet. Den här mappningen, utan explicit användning av kopplingstypen, är vad som vanligtvis menas med termen "många-till-många".

Exempel

Följande avsnitt innehåller exempel på många-till-många-relationer, inklusive den konfiguration som krävs för att uppnå varje mappning.

Tips

Koden för alla exempel nedan finns i ManyToMany.cs.

Grundläggande många-till-många

I det mest grundläggande fallet för många-till-många har entitetstyperna i varje ände av relationen både en samlingsnavigering. Till exempel:

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; } = [];
}

Den här relationen mappas av konventionen. Även om det inte behövs visas en motsvarande explicit konfiguration för den här relationen nedan som ett inlärningsverktyg:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

Även med den här explicita konfigurationen konfigureras många aspekter av relationen fortfarande av konventionen. En mer fullständig explicit konfiguration, återigen i utbildningssyfte, är:

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

Viktigt!

Försök inte att konfigurera allt fullständigt även om det inte behövs. Som du ser ovan blir koden komplicerad snabbt och det är enkelt att göra ett misstag. Och även i exemplet ovan finns det många saker i modellen som fortfarande konfigureras av konventionen. Det är inte realistiskt att tro att allt i en EF-modell alltid kan konfigureras fullständigt.

Oavsett om relationen skapas av konventionen eller med någon av de explicita konfigurationerna som visas är det resulterande mappade schemat (med SQLite) följande:

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

Tips

När du använder ett Database First-flöde för att skapa en DbContext från en befintlig databassöker EF Core 6 och senare efter det här mönstret i databasschemat och skapar en många-till-många-relation enligt beskrivningen i det här dokumentet. Det här beteendet kan ändras med hjälp av en anpassad T4-mall. För andra alternativ, se Många-till-många-relationer utan mappade kopplingsentiteter genereras nu automatiskt.

Viktigt!

För närvarande använder EF Core Dictionary<string, object> för att representera anslutningsentitetsinstanser som ingen .NET-klass har konfigurerats för. Men för att förbättra prestandan kan en annan typ användas i en framtida EF Core-version. Var inte beroende av att kopplingstypen är Dictionary<string, object> såvida inte detta har konfigurerats uttryckligen.

Många-till-många med namngiven sammankopplingstabell

I föregående exempel fick kopplingstabellen namnet PostTag av konventionen. Det kan ges ett explicit namn med UsingEntity. Till exempel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

Allt annat i mappningen förblir detsamma, med endast namnet på kopplingstabellen som ändras.

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ånga-till-många med främmande nyckelnamn i kopplingstabellen

Efter det föregående exemplet kan namnen på sekundärnyckelkolumnerna i kopplingstabellen också ändras. Det finns två sätt att göra detta på. Det första är att uttryckligen ange egenskapsnamnen för den utländska nyckeln på sammanslagningsentiteten. Till exempel:

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

Det andra sättet är att lämna egenskaperna med sina by-convention-namn, men sedan mappa dessa egenskaper till olika kolumnnamn. Till exempel:

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

I båda fallen förblir mappningen densamma, och endast kolumnnamnen för främmande nyckeln har ändrats.

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

Tips

Även om de inte visas här kan de föregående två exemplen kombineras för att ändra mappningen av kopplingstabellens namn och dess kolumnnamn för främmande nyckel.

Många-till-många med klass för kopplingsentitet

Hittills i exemplen har kopplingstabellen automatiskt mappats till en entitetstyp av delad typ. Detta tar bort behovet av att skapa en dedikerad klass för entitetstypen. Det kan dock vara användbart att ha en sådan klass så att den enkelt kan refereras till, särskilt när navigeringer eller en nyttolast (en "nyttolast" är ytterligare data i kopplingstabellen. Till exempel tidsstämpeln där en post i kopplingstabellen skapas.) läggs till i klassen, vilket visas i senare exempel nedan. Det gör du genom att först skapa en typ PostTag för kopplingsentiteten utöver de befintliga typerna för Post och Tag:

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

Tips

Klassen kan ha valfritt namn, men det är vanligt att kombinera namnen på typerna i endera änden av relationen.

Nu kan metoden UsingEntity användas för att konfigurera detta som kopplingsentitetstyp för relationen. Till exempel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

PostId och TagId hämtas automatiskt som främmande nycklar och konfigureras som den sammansatta primärnyckeln för sammanfogningstypen. De egenskaper som ska användas för utländska nycklar kan specificerat konfigureras för fall där de inte matchar EF-konventionen. Till exempel:

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

Det mappade databasschemat för kopplingstabellen i det här exemplet motsvarar strukturellt de föregående exemplen, men med några olika kolumnnamn:

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ånga-till-många med navigering för att ansluta entitet

Efter det föregående exemplet, nu när det finns en klass som representerar kopplingsentiteten, blir det enkelt att lägga till navigeringer som refererar till den här klassen. Till exempel:

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

Viktigt!

Som det visas i det här exemplet kan navigeringar till kopplingsentitetstypen användas utöver att skippa navigeringarna mellan de två ändarna i många-till-många-relationen. Det innebär att hoppa över navigeringen kan användas för att interagera med många-till-många-relationen på ett naturligt sätt, medan navigeringen till kopplingsentitetstypen kan användas när större kontroll över själva kopplingsentiteterna behövs. På sätt och vis ger den här mappningen det bästa av två världar mellan en enkel många-till-många-mappning och en mappning som mer explicit matchar databasschemat.

Inget behöver ändras i UsingEntity-anropet, eftersom navigeringen till kopplingsentiteten hämtas av konventionen. Därför är konfigurationen för det här exemplet densamma som för det senaste exemplet:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Navigeringen kan konfigureras explicit för fall där de inte kan fastställas av konventionen. Till exempel:

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

Det mappade databasschemat påverkas inte av att inkludera navigeringar i modellen:

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ånga-till-många med navigering till och från kopplingsentitet

I det föregående exemplet lades navigeringsmöjligheter till kopplingsentitetstypen från entitetstyperna i vardera änden av många-till-många-relationen. Navigering kan också läggas till i den andra riktningen eller i båda riktningarna. Till exempel:

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

Inget behöver ändras i UsingEntity-anropet, eftersom navigeringen till kopplingsentiteten hämtas av konventionen. Därför är konfigurationen för det här exemplet densamma som för det senaste exemplet:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Navigeringen kan konfigureras explicit för fall där de inte kan fastställas av konventionen. Till exempel:

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

Det mappade databasschemat påverkas inte av att inkludera navigeringar i modellen:

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ånga-till-många med navigering och ändrade främmande nycklar

I det föregående exemplet visades en många-till-många-relation med navigering till och från kopplingsentitetstypen. Det här exemplet är detsamma, förutom att de främmande nyckelelement som används också ändras. Till exempel:

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

Återigen används metoden UsingEntity för att konfigurera detta:

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

Det mappade databasschemat är nu:

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

Enkelriktad många-till-många

Det är inte nödvändigt att inkludera en navigering på båda sidor av många-till-många-relationen. Till exempel:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

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

EF behöver viss konfiguration för att veta att detta bör vara en många-till-många-relation, istället för en en-till-många. Detta görs med hjälp av HasMany och WithMany, men utan att något argument skickas på den sida som saknar navigering. Till exempel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

Om du tar bort navigeringen påverkas inte databasschemat:

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ånga-till-många- och kopplingstabell med nyttolast

I exemplen hittills har kopplingstabellen endast använts för att lagra de främmande nyckelpar som representerar varje association. Men den kan också användas för att lagra information om associationen, till exempel när den skapades. I sådana fall är det bäst att definiera en typ för kopplingsentiteten och lägga till egenskaperna "association payload" i den här typen. Det är också vanligt att skapa navigeringar till kopplingsentiteten utöver de "hoppa-över-navigeringarna" som används för många-till-många-relationen. Dessa ytterligare navigeringar gör det möjligt att enkelt referera till associeringsentiteten i koden, vilket underlättar läsning och/eller ändring av nyttolastens data. Till exempel:

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

Det är också vanligt att använda genererade värden för nyttolastegenskaper– till exempel en databastidsstämpel som anges automatiskt när associationsraden infogas. Detta kräver lite minimal konfiguration. Till exempel:

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

Resultatet mappas till ett schema av entitetstyp med en tidsstämpel som anges automatiskt när en rad infogas:

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

Tips

SQL som visas här är för SQLite. I SQL Server/Azure SQL använder du .HasDefaultValueSql("GETUTCDATE()") och för TEXT läser du datetime.

Anpassad delad entitetstyp som kopplingsentitet

I föregående exempel användes typen PostTag som kopplingsentitetstyp. Den här typen är specifik för relationen med inläggstaggar. Men om du har flera kopplingstabeller med samma form kan samma CLR-typ användas för alla. Anta till exempel att alla våra kopplingstabeller har en CreatedOn kolumn. Vi kan mappa dessa med hjälp av JoinType-klass som mappats som en entitetstyp av delad typ:

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

Vidare kan denna typ refereras till som en kopplingsentitetstyp för flera olika många-till-många-relationer. Till exempel:

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; } = [];
}

Och dessa relationer kan sedan konfigureras på lämpligt sätt för att mappa kopplingstypen till en annan tabell för varje relation:

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

Detta resulterar i följande tabeller i databasschemat:

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ånga-till-många med alternativa nycklar

Hittills har alla exempel visat att sekundärnycklarna i kopplingsentitetstypen är begränsade till de primära nycklarna för entitetstyperna på båda sidor av relationen. Varje främmande nyckel, eller båda, kan i stället begränsas till en alternativ nyckel. Tänk dig till exempel den här modellen därTag och Post ha alternativa nyckelegenskaper:

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; } = [];
}

Konfigurationen för den här modellen är:

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

Och det resulterande databasschemat, för tydlighetens skull, inklusive tabellerna med de alternativa nycklarna:

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

Konfigurationen för att använda alternativa nycklar skiljer sig något om anslutningsentitetstypen representeras av en .NET-typ. Till exempel:

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

Konfigurationen kan nu använda den allmänna metoden UsingEntity<>:

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

Och det resulterande schemat är:

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ånga-till-många och kopplingstabell med separat primär nyckel

Hittills har kopplingsentitetstypen i alla exempel en primärnyckel som består av de två främmande nyckelegenskaperna. Det beror på att varje kombination av värden för dessa egenskaper kan inträffa högst en gång. Dessa egenskaper utgör därför en naturlig primärnyckel.

Anmärkning

EF Core stöder inte duplicerade entiteter i någon samlingsnavigering.

Om du styr databasschemat finns det ingen anledning för kopplingstabellen att ha ytterligare en primär nyckelkolumn. Det är dock möjligt att en befintlig kopplingstabell kan ha en definierad primärnyckelkolumn. EF kan fortfarande mappa till detta med en viss konfiguration.

Det är kanske enklast att göra detta genom att skapa en klass som representerar kopplingsentiteten. Till exempel:

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

Den här PostTag.Id-egenskapen tas nu upp som primärnyckel enligt konventionen, så den enda konfiguration som behövs är ett anrop till UsingEntity för PostTag-typen.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

Och det resulterande schemat för kopplingstabellen är:

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

En primärnyckel kan också läggas till i kopplingsentiteten utan att definiera en klass för den. Till exempel med bara Post och Tag typer:

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; } = [];
}

Nyckeln kan läggas till med den här konfigurationen:

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

Vilket resulterar i en kopplingstabell med en separat primärnyckelkolumn:

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ånga-till-många utan kaskadborttagning

I alla exempel som visas ovan skapas de främmande nycklar som skapas mellan kopplingstabellen och de två sidorna av många-till-många-relationen med kaskaderande borttagningsbeteende. Detta är mycket användbart eftersom det innebär att om en entitet på båda sidor av relationen tas bort tas raderna i kopplingstabellen för den entiteten bort automatiskt. Eller, med andra ord, när en entitet inte längre finns, så finns dess relationer till andra entiteter inte längre.

Det är svårt att föreställa sig när det är användbart att ändra det här beteendet, men det kan göras om så önskas. Till exempel:

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

Databasschemat för kopplingstabellen använder begränsat borttagningsbeteende för begränsning på främmande nyckel.

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

Självrefererande många-till-många

Samma entitetstyp kan användas i båda ändar av en många-till-många-relation. Detta kallas för en "självrefererande" relation. Till exempel:

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

Detta motsvarar en kopplingstabell kallad PersonPerson, med båda främmande nycklarna som pekar tillbaka till People-tabellen.

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

Symmetrisk självrefererande många-till-många

Ibland är en många-till-många-relation naturligt symmetrisk. Om entitet A är relaterad till entiteten B är entiteten B alltså också relaterad till entitet A. Detta modelleras naturligt med hjälp av en enda navigering. Tänk dig till exempel fallet där person A är vän med person B och person B är vän med person A:

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

Tyvärr är detta inte lätt att mappa. Samma navigering kan inte användas för båda ändar av relationen. Det bästa du kan göra är att mappa den som en enkelriktad många-till-många-relation. Till exempel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

Men för att se till att två personer båda är relaterade till varandra måste varje person läggas till manuellt i den andra personens Friends samling. Till exempel:

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

Direkt användning av kopplingstabell

Alla exempel ovan använder EF Core många-till-många-mappningsmönstren. Det är dock också möjligt att mappa en kopplingstabell till en normal entitetstyp och bara använda de två en-till-många-relationerna för alla åtgärder.

Dessa entitetstyper representerar till exempel mappningen av två normala tabeller och kopplingstabellen utan att använda några många-till-många-relationer:

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

Detta kräver ingen särskild mappning, eftersom det här är normala entitetstyper med normala en-till-många- relationer.

Ytterligare resurser