Relace M:N

Relace M:N se používají, když je k libovolnému počtu entit jednoho typu entity přidružen libovolný počet entit stejného nebo jiného typu entity. Může mít například Post mnoho přidružených Tagsa každý Tag může být přidružen k libovolnému počtu .Posts

Principy relací M:N

Relace M:N se liší od relací 1:N a 1:1 v tom, že je nelze vyjádřit jednoduchým způsobem pomocí pouze cizího klíče. Místo toho je potřeba další typ entity k "spojení" obou stran relace. Označuje se jako "typ entity join" a mapuje se na tabulku spojení v relační databázi. Entity tohoto typu entity spojení obsahují dvojice hodnot cizího klíče, kde jedna z těchto dvojic odkazuje na entitu na jedné straně relace a druhá odkazuje na entitu na druhé straně relace. Každá entita spojení, a proto každý řádek v tabulce spojení představuje jedno přidružení mezi typy entit v relaci.

EF Core může skrýt typ entity spojení a spravovat ho na pozadí. To umožňuje, aby se navigace relace M:N používala přirozeným způsobem a podle potřeby přidávala nebo odebírala entity z každé strany. Je však užitečné pochopit, co se děje na pozadí, aby jejich celkové chování a zejména mapování na relační databázi dávalo smysl. Začněme nastavením schématu relační databáze představující vztah M:N mezi příspěvky a značkami:

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

V tomto schématu PostTag je tabulka spojení. Obsahuje dva sloupce: PostsId, což je cizí klíč k primárnímu klíči Posts tabulky a TagsIdkterý je cizím klíčem k primárnímu Tags klíči tabulky. Každý řádek v této tabulce proto představuje přidružení mezi jedním Post a jedním Tag.

Zjednodušené mapování tohoto schématu v EF Core se skládá ze tří typů entit – 1 pro každou tabulku. Pokud jsou jednotlivé typy entit reprezentované třídou .NET, můžou tyto třídy vypadat takto:

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

Všimněte si, že v tomto mapování neexistuje žádná relace M:N, ale spíše dvě relace 1:N, jedna pro každý cizí klíč definovaný v tabulce spojení. Nejedná se o nerozumný způsob, jak tyto tabulky mapovat, ale neodráží záměr tabulky spojení, která představuje jednu relaci M:N, nikoli dvě relace 1:N.

EF umožňuje přirozenější mapování prostřednictvím zavedení dvou navigace v kolekci, jedné na Post obsahující související Tagsa inverzní k Tag obsahu souvisejícího Posts. Příklad:

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

Tip

Tyto nové navigace se označují jako "přeskočit navigace", protože přeskočí entitu spojení, aby poskytovala přímý přístup na druhou stranu relace M:N.

Jak je znázorněno v následujících příkladech, relace M:N se dá tímto způsobem namapovat – to znamená s třídou .NET pro entitu spojení a s oběma navigacemi pro dvě relace 1:N a přeskočit navigaci vystavenou u typů entit. Ef však může entitu spojení spravovat transparentně, bez definované třídy .NET a bez navigace pro dvě relace 1:N. Příklad:

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

Konvence vytváření modelů EF ve výchozím nastavení Post mapují a Tag typy zobrazené zde na tři tabulky ve schématu databáze v horní části této části. Toto mapování, bez explicitního použití typu spojení, je to, co je obvykle míněno termínem "M:N".

Příklady

Následující části obsahují příklady relací M:N, včetně konfigurace potřebné k dosažení každého mapování.

Tip

Kód pro všechny níže uvedené příklady najdete v souboru ManyToMany.cs.

Základní M:N

V nejzásadnějším případě pro M:N mají typy entit na každém konci relace navigaci v kolekci. Příklad:

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

Tato relace se mapuje podle konvence. I když není potřeba, ekvivalentní explicitní konfigurace pro tento vztah se zobrazí níže jako výukový nástroj:

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

I když je tato explicitní konfigurace, konvence stále konvence konfiguruje mnoho aspektů relace. Konkrétnější konfigurace, která je opět určená pro účely výuky, je:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

Důležité

Nepokoušejte se plně konfigurovat všechno, i když není potřeba. Jak je vidět výše, kód se rychle zkomplikuje a snadno udělá chybu. A i v příkladu výše existuje mnoho věcí v modelu, které jsou stále nakonfigurované konvencí. Není reálné si myslet, že vše v modelu EF může být vždy plně nakonfigurované explicitně.

Bez ohledu na to, jestli je relace vytvořená konvencí nebo pomocí některé z zobrazených explicitních konfigurací, je výsledné mapované schéma (pomocí 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);

Tip

Při použití toku Database First k vygenerování dbContext z existující databáze EF Core 6 a novější hledá tento vzor ve schématu databáze a vygeneruje relaci M:N, jak je popsáno v tomto dokumentu. Toto chování lze změnit pomocí vlastní šablony T4. Další možnosti najdete v tématu Relace M:N bez namapovaných entit spojení se teď vygenerují.

Důležité

EF Core v současné době používá Dictionary<string, object> k reprezentaci instancí entity join, pro které nebyla nakonfigurována žádná třída .NET. Pokud ale chcete zvýšit výkon, může být v budoucí verzi EF Core použit jiný typ. Nespoléhejte na typu Dictionary<string, object> spojení, pokud není explicitně nakonfigurovaný.

N:N s pojmenovanou tabulkou spojení

V předchozím příkladu se tabulka spojení pojmenovala PostTag podle konvence. Lze mu dát explicitní název s UsingEntity. Příklad:

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

Všechno ostatní o mapování zůstává stejné, pouze název tabulky spojení se mění:

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

N:N s názvy cizích klíčů tabulky join

Podle předchozího příkladu je možné změnit také názvy sloupců cizího klíče v tabulce spojení. Můžete to provést dvěma způsoby. První je explicitně zadat názvy vlastností cizího klíče v entitě join. Příklad:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

Druhým způsobem je ponechat vlastnosti s jejich názvy podle konvence, ale pak tyto vlastnosti namapovat na různé názvy sloupců. Příklad:

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

V obou případech mapování zůstane stejné, přičemž se změnily pouze názvy sloupců cizího klíče:

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

Tip

I když se zde nezobrazují, předchozí dva příklady je možné zkombinovat a mapovat změnu názvu tabulky spojení a názvů sloupců cizího klíče.

M:N s třídou pro spojitou entitu

Zatím v příkladech byla tabulka spojení automaticky namapována na typ entity sdíleného typu. Tím se odebere potřeba vytvořit vyhrazenou třídu pro typ entity. Může být ale užitečné mít takovou třídu, aby se na ni dala snadno odkazovat, zejména pokud jsou navigace nebo datová část přidány do třídy, jak je znázorněno v dalších příkladech níže. Uděláte to tak, že nejprve vytvoříte typ PostTag pro entitu join kromě existujících typů pro Post a 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; }
}

Tip

Třída může mít libovolný název, ale je běžné kombinovat názvy typů na obou konci relace.

Teď můžete tuto metodu UsingEntity použít ke konfiguraci jako typu entity join pro relaci. Příklad:

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

TagId Automaticky PostId se vyberou jako cizí klíče a nakonfigurují se jako složený primární klíč pro typ entity join. Vlastnosti, které se mají použít pro cizí klíče, je možné explicitně nakonfigurovat pro případy, kdy neodpovídají konvenci EF. Příklad:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

Mapované schéma databáze pro tabulku spojení v tomto příkladu je strukturálně ekvivalentní předchozím příkladům, ale s některými různými názvy sloupců:

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 s navigacemi pro spojení entity

Dále z předchozího příkladu teď, když existuje třída představující entitu join, je snadné přidat navigace, které odkazují na tuto třídu. Příklad:

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

Důležité

Jak je znázorněno v tomto příkladu, navigace na typ entity spojení je možné použít kromě přeskočení navigace mezi dvěma konci relace M:N. To znamená, že přeskočení navigace lze použít k interakci s relací M:N přirozeným způsobem, zatímco navigace k typu entity spojení je možné použít, když je potřeba větší kontrolu nad samotnými entitami spojení. Toto mapování poskytuje nejlepší z obou světů mezi jednoduchým mapováním M:N a mapováním, které explicitněji odpovídá schématu databáze.

Ve UsingEntity volání není potřeba nic měnit, protože navigace k entitě join se vybírají podle konvence. Proto je konfigurace pro tento příklad stejná jako v posledním příkladu:

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

Navigace je možné nakonfigurovat explicitně pro případy, kdy je nelze určit konvencí. Příklad:

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

Schéma mapované databáze není ovlivněno zahrnutím navigace v modelu:

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 s navigacemi do entity spojení a z ní

Předchozí příklad přidal navigaci do typu entity spojení z typů entit na obou koncích relace M:N. Navigace se dají přidat také v opačném směru nebo v obou směrech. Příklad:

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

Ve UsingEntity volání není potřeba nic měnit, protože navigace k entitě join se vybírají podle konvence. Proto je konfigurace pro tento příklad stejná jako v posledním příkladu:

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

Navigace je možné nakonfigurovat explicitně pro případy, kdy je nelze určit konvencí. Příklad:

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

Schéma mapované databáze není ovlivněno zahrnutím navigace v modelu:

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 s navigacemi a změněnými cizími klíči

Předchozí příklad ukázal M:N s navigacemi do a z typu entity join. Tento příklad je stejný, s výjimkou toho, že použité vlastnosti cizího klíče jsou také změněny. Příklad:

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

UsingEntity Metoda se opět používá ke konfiguraci:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

Mapované schéma databáze je teď:

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

Jednosměrné M:N

Poznámka:

V EF Core 7 byly zavedeny jednosměrné relace M:N. V dřívějších verzích je možné privátní navigaci použít jako alternativní řešení.

Není nutné zahrnout navigaci na obou stranách relace M:N. Příklad:

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

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

EF potřebuje určitou konfiguraci, aby věděla, že by to měla být relace M:N, a ne 1:N. To se provádí pomocí HasMany a WithMany, ale bez argumentu předaného na straně bez navigace. Příklad:

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

Odebrání navigace nemá vliv na schéma databáze:

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

Tabulka M:N a spojení s datovou částí

V dosud uvedených příkladech byla tabulka spojení použita pouze k ukládání dvojic cizích klíčů představujících každé přidružení. Dá se ale také použít k ukládání informací o přidružení – například k době, kdy byla vytvořena. V takových případech je nejlepší definovat typ pro entitu join a přidat do tohoto typu vlastnosti "datová část přidružení". Kromě "přeskakování navigace" používaných pro relaci M:N je také běžné vytvářet navigace pro entitu spojení. Tyto další navigace umožňují entitě spojení snadno odkazovat z kódu, což usnadňuje čtení a/nebo změnu dat datové části. Příklad:

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

Pro vlastnosti datové části je také běžné používat vygenerované hodnoty – například časové razítko databáze, které se automaticky nastaví při vložení řádku přidružení. To vyžaduje určitou minimální konfiguraci. Příklad:

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

Výsledek se mapuje na schéma typu entity s časovým razítkem nastaveným automaticky při vložení řádku:

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

Tip

Zde uvedený sql je určen pro SQLite. Na SQL Serveru nebo Azure SQL použijte .HasDefaultValueSql("GETUTCDATE()") a pro TEXT čtení datetime.

Vlastní typ entity sdíleného typu jako entita spojení

Předchozí příklad použil typ PostTag jako typ entity join. Tento typ je specifický pro relaci post-tags. Pokud ale máte více spojování tabulek se stejným obrazcem, můžete pro všechny použít stejný typ CLR. Představte si například, že všechny naše tabulky spojení mají CreatedOn sloupec. Můžeme je mapovat pomocí JoinType třídy mapované jako typu entity sdíleného typu:

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

Na tento typ se pak dá odkazovat jako na typ entity spojení několika různými relacemi M:N. Příklad:

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

Tyto relace je pak možné správně nakonfigurovat tak, aby typ spojení namapoval na jinou tabulku pro každou relaci:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.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",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

Výsledkem jsou následující tabulky ve schématu databáze:

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 s alternativními klíči

Zatím všechny příklady ukázaly cizí klíče v typu entity spojení omezené na primární klíče typů entit na obou stranách relace. Každý cizí klíč nebo obojí může být místo toho omezen na alternativní klíč. Představte si například tento model, kdeTag a Post mají alternativní vlastnosti klíče:

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

Konfigurace pro tento model je:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

Výsledné schéma databáze, aby bylo jasné, včetně tabulek s alternativními klíči:

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

Konfigurace pro použití alternativních klíčů se mírně liší, pokud je typ entity spojení reprezentován typem .NET. Příklad:

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

Konfigurace teď může používat obecnou UsingEntity<> metodu:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

Výsledné schéma je:

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

Tabulka M:N a tabulka spojení s odděleným primárním klíčem

Zatím má typ entity join ve všech příkladech primární klíč složený ze dvou vlastností cizího klíče. Důvodem je to, že každá kombinace hodnot pro tyto vlastnosti může nastat maximálně jednou. Tyto vlastnosti proto tvoří přirozený primární klíč.

Poznámka:

EF Core nepodporuje duplicitní entity v žádné navigaci v kolekci.

Pokud řídíte schéma databáze, není důvod, proč by tabulka spojení měla další sloupec primárního klíče, je však možné, že existující tabulka spojení může mít definovaný sloupec primárního klíče. EF se na to může stále mapovat s určitou konfigurací.

Je to možná nejjednodušší tím, že vytvoříte třídu představující entitu join. Příklad:

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

Tato PostTag.Id vlastnost se teď přebírá jako primární klíč podle konvence, takže jedinou potřebou konfigurace je volání UsingEntity typu PostTag :

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

Výsledné schéma pro tabulku spojení je:

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

Primární klíč lze také přidat do entity join bez definování třídy pro ni. Například s pouhými Post a Tag typy:

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

Klíč lze přidat pomocí této konfigurace:

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

Výsledkem je tabulka spojení se samostatným sloupcem primárního klíče:

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 bez kaskádového odstranění

Ve všech výše uvedených příkladech se vytvoří cizí klíče vytvořené mezi tabulkou spojení a dvěma stranami relace M:N s kaskádovým chováním odstranění . To je velmi užitečné, protože to znamená, že pokud je entita na jedné straně relace odstraněna, řádky v tabulce spojení pro danou entitu se automaticky odstraní. Jinými slovy, pokud už entita neexistuje, pak už její vztahy s jinými entitami neexistují.

Je těžké si představit, kdy je užitečné toto chování změnit, ale v případě potřeby ho můžete provést. Příklad:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

Schéma databáze pro tabulku spojení používá omezené chování odstranění v omezení cizího klíče:

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

Odkazování na M:N

Stejný typ entity lze použít na obou koncích relace M:N; označuje se jako vztah odkazující na sebe. Příklad:

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

Tím se namapuje na spojování tabulky s PersonPersonoběma cizími klíči odkazujícími zpět na People tabulku:

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

Symetrické samoobslužné odkazování na M:N

Někdy je vztah M:N přirozeně symetrický. To znamená, že pokud entita A souvisí s entitou B, pak entita B souvisí také s entitou A. To je přirozeně modelováno pomocí jediné navigace. Představte si například případ, kdy je osoba A přátelé s osobou B, pak osoba B je přátelé s osobou A:

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

Bohužel to není snadné mapovat. Stejnou navigaci nelze použít pro oba konce relace. Nejlepší možností je namapovat ho jako jednosměrnou relaci M:N. Příklad:

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

Pokud se ale chcete ujistit, že dva lidé spolu vzájemně souvisí, bude potřeba ručně přidat každou osobu do kolekce druhé osoby Friends . Příklad:

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

Přímé použití tabulky spojení

Všechny výše uvedené příklady využívají vzory mapování EF Core M:N. Je však také možné namapovat tabulku spojení na normální typ entity a pouze použít dvě relace 1:N pro všechny operace.

Například tyto typy entit představují mapování dvou normálních tabulek a spojit tabulku bez použití relací M:N:

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

To nevyžaduje žádné zvláštní mapování, protože se jedná o normální typy entit s normálními relacemi 1:N .

Další materiály

  • Relace .NET Data Community Standup s podrobným rozborem M:N a infrastrukturou, která ji podporuje.