Udostępnij za pośrednictwem


Relacje wiele-do-wielu

Relacje wiele-do-wielu są używane, gdy dowolna liczba jednostek jednego typu jednostki jest skojarzona z dowolną liczbą jednostek tego samego lub innego typu jednostki. Na przykład Post może mieć wiele skojarzonych elementów Tags, a każda Tag z nich może być skojarzona z dowolną liczbą Posts.

Zrozumienie relacji wiele-do-wielu

Relacje wiele-do-wielu różnią się od relacji jeden do wielu i jeden do jednego , ponieważ nie mogą być reprezentowane w prosty sposób przy użyciu tylko klucza obcego. Zamiast tego potrzebny jest dodatkowy typ jednostki do "sprzężenia" obu stron relacji. Jest to nazywane "typem jednostki połączenia" i mapuje na "tabelę połączeń" w relacyjnej bazie danych. Jednostki tego typu jednostki sprzężenia zawierają pary wartości klucza obcego, gdzie jedna z każdej pary wskazuje jednostkę po jednej stronie relacji, a druga wskazuje jednostkę po drugiej stronie relacji. Każda jednostka sprzężenia, a zatem każdy wiersz w tabeli sprzężenia reprezentuje jedno skojarzenie między typami jednostek w relacji.

Program EF Core może ukryć typ jednostki sprzężenia i zarządzać nim za kulisami. Dzięki temu nawigacja relacji wiele-do-wielu może być używana w naturalny sposób, dodając lub usuwając elementy z każdej strony, zgodnie z potrzebami. Warto jednak zrozumieć, co dzieje się za kulisami, aby ich ogólne zachowanie, a w szczególności mapowanie relacyjnej bazy danych miało sens. Zacznijmy od ustawienia schematu relacyjnej bazy danych, aby odzwierciedlić relację wiele-do-wielu między postami a tagami.

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

W tym schemacie PostTag jest tabela sprzężenia. Zawiera dwie kolumny: PostsId, która jest kluczem obcym do klucza podstawowego Posts tabeli, i TagsId, który jest kluczem obcym do klucza podstawowego Tags tabeli. W związku z tym każdy wiersz w tej tabeli reprezentuje skojarzenie między jednym Post i jednym Tagwierszem .

Uproszczone mapowanie tego schematu w programie EF Core składa się z trzech typów jednostek — jeden dla każdej tabeli. Jeśli każdy z tych typów jednostek jest reprezentowany przez klasę .NET, te klasy mogą wyglądać następująco:

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

Zwróć uwagę, że w tym mapowaniu nie ma relacji wiele do wielu, lecz raczej dwie relacje jeden do wielu, po jednej dla każdego z kluczy obcych zdefiniowanych w tabeli sprzężeń. Nie jest to nieuzasadniony sposób mapowania tych tabel, ale nie odzwierciedla intencji tabeli sprzężenia, która reprezentuje pojedynczą relację wiele-do-wielu, a nie dwie relacje jeden do wielu.

Program EF umożliwia bardziej naturalne mapowanie poprzez wprowadzenie dwóch nawigacji kolekcji: jednej na Post zawierającej powiązany z nią element Tags, oraz odwrotnej na Tag zawierającej swój powiązany element Posts. Przykład:

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

Wskazówka

Nawigacje te są znane jako "nawigacje pomijania", ponieważ pomijają element łączący, aby zapewnić bezpośredni dostęp do drugiej części relacji wiele-do-wielu.

Jak pokazano w powyższym przykładzie, relację wiele do wielu można zamapować w ten sposób: używając klasy .NET dla jednostki sprzężenia oraz uwzględniając obydwie nawigacje dla dwóch relacji jeden do wielu oraz pomijane nawigacje uwidocznione na typach jednostek. Jednak program EF może w sposób niewidoczny zarządzać jednostką sprzężenia bez zdefiniowanej dla niej klasy .NET i bez nawigacji dla dwóch relacji jeden do wielu. Przykład:

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

Rzeczywiście konwencje tworzenia modelu EF będą domyślnie mapować typy pokazane tutaj na trzy tabele w schemacie baz danych na górze tej sekcji. To mapowanie, bez jawnego użycia typu sprzężenia, jest zwykle oznaczane terminem "wiele do wielu".

Przykłady

Poniższe sekcje zawierają przykłady relacji wiele-do-wielu, w tym konfigurację wymaganą do osiągnięcia każdego mapowania.

Wskazówka

Kod dla wszystkich poniższych przykładów można znaleźć w ManyToMany.cs.

Podstawowa liczba-do-wielu

W najbardziej podstawowym przypadku relacji wielu-do-wielu, typy jednostek na każdym końcu relacji mają nawigację w formie kolekcji. Przykład:

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

Ta relacja jest mapowana zgodnie z konwencją. Mimo że nie jest to wymagane, równoważna jawna konfiguracja tej relacji jest pokazana poniżej jako narzędzie szkoleniowe:

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

Nawet w przypadku tej jawnej konfiguracji wiele aspektów relacji jest nadal konfigurowanych zgodnie z konwencją. Bardziej pełną jawną konfiguracją, ponownie dla celów szkoleniowych, jest:

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

Ważne

Nie próbuj w pełni skonfigurować wszystkiego, nawet jeśli nie jest to konieczne. Jak widać powyżej, kod szybko się komplikuje i łatwo popełnia błąd. Nawet w powyższym przykładzie istnieje wiele elementów w modelu, które są nadal konfigurowane zgodnie z konwencją. Nie można zakładać, że wszystko w modelu EF zawsze może być jawnie w pełni skonfigurowane.

Niezależnie od tego, czy relacja jest kompilowana zgodnie z konwencją, czy przy użyciu jednej z pokazanych jawnych konfiguracji, wynikowy schemat mapowany (przy użyciu sqlite) jest:

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

Wskazówka

Podczas używania przepływu Database First do tworzenia szkieletu obiektu DbContext z istniejącej bazy danych EF Core 6 i nowsze wersje wyszukują ten wzorzec w schemacie bazy danych i tworzą szkielet relacji wiele do wielu zgodnie z opisem w tym dokumencie. To zachowanie można zmienić za pomocą niestandardowego szablonu T4. Aby uzyskać inne opcje, zobacz Relacje wiele-do-wielu bez mapowanych jednostek sprzężenia są teraz szkieletowe.

Ważne

Obecnie program EF Core używa Dictionary<string, object> do reprezentowania wystąpień łączenia jednostek, dla których nie skonfigurowano żadnej klasy .NET. Jednak w celu zwiększenia wydajności można użyć innego typu w przyszłej wersji platformy EF Core. Nie polegaj na typie sprzężenia Dictionary<string, object>, o ile nie zostało to jawnie skonfigurowane.

Wiele do wielu z nazwaną tabelą połączeń

W poprzednim przykładzie tabela połączeń została nazwana zgodnie z konwencją jako PostTag. Można nadać mu jawną nazwę z UsingEntity. Przykład:

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

Wszystkie inne informacje o mapowaniu pozostają takie same, a tylko nazwa tabeli sprzężenia zmienia się:

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

Wiele do wielu z nazwami kluczy obcych tabeli sprzężenia

Kontynuując poprzedni przykład, można również zmienić nazwy kolumn kluczy obcych w tabeli łączeń. Istnieją dwa sposoby, aby to zrobić. Pierwszym z nich jest jawne określenie nazw właściwości klucza obcego w jednostce połączenia. Przykład:

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

Drugim sposobem jest pozostawienie właściwości z ich nazwami zgodnymi z konwencją, ale następnie przypisać te właściwości do różnych nazw kolumn. Przykład:

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

W obu przypadkach mapowanie pozostaje takie samo, a tylko nazwy kolumn klucza obcego uległy zmianie:

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

Wskazówka

Chociaż nie pokazano tutaj, poprzednie dwa przykłady można połączyć, aby przemapować zmianę nazwy tabeli łączącej oraz nazw kolumn jego kluczy obcych.

Wiele do wielu z klasą dla jednostki sprzężenia

Do tej pory w przykładach tabela połączeń została automatycznie zamapowana na udostępniony typ jednostki. Spowoduje to usunięcie potrzeby utworzenia dedykowanej klasy dla typu jednostki. Jednak może być przydatne posiadanie takiej klasy, aby można było łatwo się do niej odwoływać, zwłaszcza gdy nawigacja lub ładunek ("ładunek" to wszelkie dodatkowe dane w tabeli sprzężenia. Na przykład znacznik czasu, w którym jest tworzony wpis w tabeli sprzężenia). są dodawane do klasy, jak pokazano w poniższych przykładach. Aby to zrobić, najpierw utwórz typ PostTag dla jednostki sprzężenia oprócz istniejących typów dla Post i 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; }
}

Wskazówka

Klasa może mieć dowolną nazwę, ale zwykle łączy się nazwy typów z obu końców relacji.

UsingEntity Teraz można użyć metody , aby skonfigurować tę metodę jako typ jednostki sprzężenia dla relacji. Przykład:

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

Elementy PostId i TagId są automatycznie rozpoznawane jako klucze obce i są konfigurowane jako złożony klucz podstawowy dla typu jednostki połączeniowej. Właściwości do użycia dla kluczy obcych można jawnie skonfigurować w przypadkach, w których nie są zgodne z konwencją EF. Przykład:

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

Mapowany schemat bazy danych dla tabeli sprzężenia w tym przykładzie jest strukturalnie odpowiednikiem poprzednich przykładów, ale z różnymi nazwami kolumn:

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

Wiele do wielu z nawigacjami w celu sprzężenia jednostki

Po wykonaniu poprzedniego przykładu, teraz, gdy istnieje klasa reprezentująca jednostkę sprzężenia, łatwo jest dodać nawigacje odwołujące się do tej klasy. Przykład:

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

Ważne

Jak pokazano w tym przykładzie, nawigacje do typu jednostki sprzężenia mogą być używane oprócz pomijania nawigacji między dwoma końcami relacji wiele-do-wielu. Oznacza to, że nawigacje pomijające mogą służyć do interakcji z relacją wiele-do-wielu w naturalny sposób, podczas gdy nawigacje do typu jednostki łączącej mogą być używane, gdy potrzebna jest większa kontrola nad jednostkami łączącymi. W pewnym sensie, to mapowanie zapewnia najlepsze z obu światów, łącząc proste mapowanie wiele-do-wielu z mapowaniem, które wyraźniej pasuje do schematu bazy danych.

W wywołaniu UsingEntity nie trzeba nic zmieniać, ponieważ nawigacje do jednostki sprzężenia są pobierane zgodnie z konwencją. W związku z tym konfiguracja dla tego przykładu jest taka sama jak w przypadku ostatniego przykładu:

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

Nawigacje można skonfigurować jawnie dla przypadków, w których nie można ich określić zgodnie z konwencją. Przykład:

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

Uwzględnienie nawigacji w modelu nie ma wpływu na mapowany schemat bazy danych.

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

Wiele do wielu z nawigacjami do i z jednostki join

W poprzednim przykładzie dodano nawigacje do typu łączącego jednostki z typów jednostek na obu końcach relacji wiele-do-wielu. Nawigacje można również dodawać w innym kierunku lub w obu kierunkach. Przykład:

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

W wywołaniu UsingEntity nie trzeba nic zmieniać, ponieważ nawigacje do jednostki sprzężenia są pobierane zgodnie z konwencją. W związku z tym konfiguracja dla tego przykładu jest taka sama jak w przypadku ostatniego przykładu:

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

Nawigacje można skonfigurować jawnie dla przypadków, w których nie można ich określić zgodnie z konwencją. Przykład:

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

Uwzględnienie nawigacji w modelu nie ma wpływu na mapowany schemat bazy danych.

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

Wiele do wielu z nawigacjami i zmienionymi kluczami obcymi

W poprzednim przykładzie pokazano relację wiele-do-wielu z nawigacjami do i od typu jednostki łączenia. Ten przykład jest taki sam, z tą różnicą, że używane właściwości klucza obcego również są zmieniane. Przykład:

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

Ponownie metoda UsingEntity jest używana do skonfigurowania tego:

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

Mapowany schemat bazy danych jest teraz:

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

Jednokierunkowa relacja wiele do wielu

Nie jest konieczne umieszczenie nawigacji po obu stronach relacji typu wiele do wielu. Przykład:

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

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

Entity Framework potrzebuje pewnej konfiguracji, aby wiedzieć, że powinna to być relacja wiele-do-wielu, a nie relacja jeden-do-wielu. Odbywa się to przy użyciu znaczników HasMany i WithMany, ale bez przekazania argumentu po stronie bez nawigacji. Przykład:

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

Usunięcie nawigacji nie ma wpływu na schemat bazy danych:

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

Wiele do wielu i tabela powiązań z zawartością

W przykładach do tej pory tabela łącząca została użyta tylko do przechowywania par kluczy obcych reprezentujących każde powiązanie. Można go jednak również użyć do przechowywania informacji o skojarzeniu — na przykład czasu jego utworzenia. W takich przypadkach najlepiej zdefiniować typ dla jednostki połączenia i dodać właściwości "ładunku skojarzenia" do tego typu. Często tworzy się również nawigacje do jednostki połączeniowej, oprócz nawigacji pomijających używanych w relacjach wiele-do-wielu. Te dodatkowe nawigacje umożliwiają łatwe odwoływanie się do jednostki sprzężenia z kodu, ułatwiając odczytywanie i/lub zmienianie danych ładunku. Przykład:

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

Często używa się również wygenerowanych wartości dla właściwości ładunku — na przykład sygnatury czasowej bazy danych, która jest ustawiana automatycznie po wstawieniu wiersza skojarzenia. Wymaga to minimalnej konfiguracji. Przykład:

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

Wynik jest mapowany na schemat typu encji, a znacznik czasu jest ustawiany automatycznie podczas wstawiania wiersza.

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

Wskazówka

Pokazany tutaj język SQL jest przeznaczony dla sqlite. W SQL Server/Azure SQL użyj .HasDefaultValueSql("GETUTCDATE()") i dla TEXT odczytuj datetime.

Niestandardowy typ jednostki wspólnej jako jednostka łącząca

W poprzednim przykładzie użyto typu PostTag jako typu jednostki sprzężenia. Ten typ jest specyficzny dla relacji posts-tags. Jeśli jednak masz wiele tabel sprzężenia o tym samym kształcie, można użyć tego samego typu CLR dla wszystkich z nich. Załóżmy na przykład, że wszystkie nasze tabele połączeń mają kolumnę CreatedOn . Można je mapować przy użyciu JoinType klasy mapowanej jako współdzielony typ jednostki:

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

Ten typ może być następnie przywołyny jako typ jednostki sprzężenia przez wiele różnych relacji wiele-do-wielu. Przykład:

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

Te relacje można skonfigurować odpowiednio do mapowania typu sprzężenia na inną tabelę dla każdej relacji:

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

Spowoduje to wykonanie następujących tabel w schemacie bazy danych:

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

Wiele do wielu z kluczami alternatywnymi

Do tej pory wszystkie przykłady pokazały, że klucze obce w typie powiązanej jednostki są ograniczone do kluczy podstawowych typów jednostek po każdej stronie relacji. Każdy klucz obcy lub oba te elementy mogą być ograniczone do klucza alternatywnego. Rozważmy na przykład ten model, w którymTag i Post mają właściwości klucza alternatywnego:

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

Konfiguracja tego modelu to:

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

Wynikowy schemat bazy danych, dla jasności, zawiera również tabele z kluczami alternatywnymi.

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

Konfiguracja używania kluczy alternatywnych jest nieco inna, jeśli typ jednostki sprzężenia jest reprezentowany przez typ platformy .NET. Przykład:

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

Konfiguracja może teraz używać metody ogólnej 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));
}

Wynikowy schemat to:

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

Wiele do wielu i tabela sprzężenia z oddzielnym kluczem podstawowym

Do tej pory typ jednostki sprzężenia we wszystkich przykładach ma klucz podstawowy składający się z dwóch właściwości klucza obcego. Jest to spowodowane tym, że każda kombinacja wartości dla tych właściwości może wystąpić co najwyżej raz. Te właściwości tworzą zatem naturalny klucz podstawowy.

Uwaga / Notatka

Program EF Core nie obsługuje zduplikowanych jednostek w żadnej nawigacji kolekcji.

Jeśli kontrolujesz schemat bazy danych, nie ma powodu, aby tabela sprzężenia miała dodatkową kolumnę klucza podstawowego, jednak istnieje możliwość, że istniejąca tabela sprzężenia może mieć zdefiniowaną kolumnę klucza podstawowego. Program EF nadal może mapować na tę konfigurację.

Być może najłatwiej jest to zrobić, tworząc klasę reprezentującą jednostkę sprzężenia. Przykład:

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

Ta PostTag.Id właściwość jest teraz pobierana jako klucz podstawowy zgodnie z konwencją, więc jedyną wymaganą konfiguracją jest wywołanie UsingEntity dla PostTag typu:

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

Schemat tabeli łączenia danych to:

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

Klucz podstawowy można również dodać do jednostki sprzężenia bez definiowania dla niej klasy. Na przykład tylko z typami Post i 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; } = [];
}

Klucz można dodać przy użyciu tej konfiguracji:

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

Co powoduje utworzenie tabeli sprzężenia z oddzielną kolumną klucza podstawowego:

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

Wiele-do-wielu bez kaskadowego usuwania

We wszystkich przedstawionych powyżej przykładach klucze obce utworzone między tabelą łączącą a dwiema stronami relacji wiele-do-wielu są tworzone przy użyciu zachowania kaskadowego usuwania. Jest to bardzo przydatne, ponieważ oznacza to, że jeśli jednostka po obu stronach relacji zostanie usunięta, wiersze w tabeli sprzężeń dla tej jednostki zostaną automatycznie usunięte. Lub, innymi słowy, gdy jednostka już nie istnieje, jej relacje z innymi jednostkami również nie istnieją.

Trudno sobie wyobrazić, kiedy warto zmienić to zachowanie, ale można to zrobić w razie potrzeby. Przykład:

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

Schemat bazy danych dla tabeli sprzężenia używa ograniczonego zachowania usuwania w ograniczeniu klucza obcego:

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

Samozwańce odwoływanie się do wielu

Ten sam typ jednostki może być używany na obu końcach relacji wiele-do-wielu; jest to nazywane relacją "odwołującą się do siebie". Przykład:

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

Odnosi się to do tabeli sprzężenia o nazwie PersonPerson, z oboma kluczami obcymi wskazującymi na tabelę People.

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

Symetryczne samoprzywoływanie wielu do wielu

Czasami relacja wiele do wielu jest naturalnie symetryczna. Oznacza to, że jeśli jednostka A jest powiązana z jednostką B, jednostka B jest również powiązana z jednostką A. Jest to naturalnie modelowane przy użyciu jednej nawigacji. Załóżmy na przykład, że w przypadku, gdy osoba A jest przyjaciołami z osobą B, a osoba B jest przyjaciółmi osoby A:

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

Niestety, nie jest to łatwe do mapowania. Nie można używać tej samej nawigacji dla obu końców relacji. Najlepsze, co można zrobić, to mapować go jako jednokierunkową relację wiele-do-wielu. Przykład:

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

Aby jednak upewnić się, że dwie osoby są ze sobą powiązane, każda osoba musi zostać ręcznie dodana do kolekcji Friends innej osoby. Przykład:

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

Bezpośrednie użycie tabeli połączeń

Wszystkie powyższe przykłady korzystają z wzorców mapowania wiele-do-wielu platformy EF Core. Jednak istnieje również możliwość mapowania tabeli sprzężenia na normalny typ jednostki i po prostu użyć dwóch relacji jeden do wielu dla wszystkich operacji.

Na przykład te typy jednostek reprezentują mapowanie dwóch normalnych tabel i tabeli sprzężenia bez używania relacji wiele-do-wielu:

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

Nie wymaga to specjalnego mapowania, ponieważ są to normalne typy jednostek z normalnymi relacjami jeden do wielu .

Dodatkowe zasoby