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.

Opis 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 sprzężenia" i mapuje na "tabelę sprzężenia" 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 jednostki 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 konfiguracji schematu relacyjnej bazy danych, aby reprezentować relację wiele-do-wielu między wpisami i 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, ale raczej dwóch relacji jeden do wielu, po jednym 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, po jednym na Post temat jego powiązanego Tagselementu i odwrotnego elementu Tag zawierającego powiązanego z Postsnią elementu . Na 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!;
}

Napiwek

Te nowe nawigacje są znane jako "pomijanie nawigacji", ponieważ pomijają jednostkę sprzężenia w celu zapewnienia bezpośredniego dostępu do drugiej strony relacji wiele-do-wielu.

Jak pokazano w poniższych przykładach, relację wiele do wielu można zamapować w ten sposób — z klasą platformy .NET dla jednostki sprzężenia oraz z obydwoma nawigacjami dla dwóch relacji jeden do wielu i pomijania nawigacji uwidocznionych 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. Na 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ć Post typy i Tag wyświetlane tutaj na trzy tabele w schemacie bazy danych w górnej części 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.

Napiwek

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

Podstawowa liczba-do-wielu

W najbardziej podstawowym przypadku dla wielu typy jednostek na każdym końcu relacji mają nawigację po kolekcji. Na 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",
            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"));
}

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 myśleć, że wszystko w modelu EF zawsze może być 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);

Napiwek

W przypadku używania przepływu Database First do tworzenia szkieletu obiektu DbContext z istniejącej bazy danych program EF Core 6 i nowsze wyszukuje ten wzorzec w schemacie bazy danych i szkieletach 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> funkcji do reprezentowania wystąpień jednostek sprzężenia, dla których nie skonfigurowano żadnej klasy platformy .NET. Jednak w celu zwiększenia wydajności można użyć innego typu w przyszłej wersji platformy EF Core. Nie należy zależeć od typu Dictionary<string, object> sprzężenia, chyba że zostało to jawnie skonfigurowane.

Wiele do wielu z nazwaną tabelą sprzężenia

W poprzednim przykładzie tabela sprzężenia została nazwana zgodnie PostTag z konwencją. Można nadać mu jawną nazwę z UsingEntity. Na 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

Po wykonaniu poprzedniego przykładu można również zmienić nazwy kolumn kluczy obcych w tabeli sprzężenia. Istnieją dwa sposoby, aby to zrobić. Pierwszym z nich jest jawne określenie nazw właściwości klucza obcego w jednostce sprzężenia. Na przykład:

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

Drugim sposobem jest pozostawienie właściwości z ich nazwami konwencji, ale następnie zamapować te właściwości na różne nazwy kolumn. Na 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);

Napiwek

Chociaż nie pokazano tutaj, poprzednie dwa przykłady można połączyć w celu mapowania zmiany nazwy tabeli sprzężenia i jego nazw kolumn klucza obcego.

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

Do tej pory w przykładach tabela sprzężenia została automatycznie zamapowana na typ jednostki typu udostępnionego. 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ć, szczególnie w przypadku dodania nawigacji lub ładunku 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; }
}

Napiwek

Klasa może mieć dowolną nazwę, ale często łączy nazwy typów na dowolnym końcu relacji.

UsingEntity Teraz można użyć metody , aby skonfigurować tę metodę jako typ jednostki sprzężenia dla relacji. Na 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 pobierane jako klucze obce i są konfigurowane jako złożony klucz podstawowy dla typu jednostki sprzężenia. 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. Na przykład:

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

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. Na 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 nawigacja pomijania może służyć do interakcji z relacją wiele-do-wielu w naturalny sposób, podczas gdy nawigacje do typu jednostki sprzężenia mogą być używane, gdy potrzebna jest większa kontrola nad jednostkami sprzężenia. W sensie to mapowanie zapewnia najlepsze z obu światów między prostym mapowaniem wiele-do-wielu, a mapowaniem, które bardziej jawnie 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ą. Na przykład:

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

Nie ma to wpływu na mapowany schemat bazy danych, uwzględniając nawigacje w 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);

Wiele do wielu z nawigacjami do i z jednostki join

W poprzednim przykładzie dodano nawigacje do typu jednostki sprzężenia z typów jednostek na jednym końcu relacji wiele do wielu. Nawigacje można również dodawać w innym kierunku lub w obu kierunkach. Na 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ą. Na przykład:

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

Nie ma to wpływu na mapowany schemat bazy danych, uwzględniając nawigacje w 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);

Wiele do wielu z nawigacjami i zmienionymi kluczami obcymi

W poprzednim przykładzie pokazano wiele do wielu z nawigacjami do i z typu jednostki sprzężenia. Ten przykład jest taki sam, z tą różnicą, że używane właściwości klucza obcego również są zmieniane. Na 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 jest używana do skonfigurowania UsingEntity następującej metody:

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

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

Jednokierunkowy wiele do wielu

Uwaga

Jednokierunkowe relacje wiele-do-wielu zostały wprowadzone w programie EF Core 7. We wcześniejszych wersjach jako obejście można użyć prywatnej nawigacji.

Nie jest konieczne dołączenie nawigacji po obu stronach relacji wiele do wielu. Na przykład:

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

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

Ef potrzebuje pewnej konfiguracji, aby wiedzieć, że powinna to być relacja wiele-do-wielu, a nie jeden do wielu. Odbywa się to przy użyciu elementów HasMany i WithMany, ale bez argumentu przekazanego po stronie bez nawigacji. Na 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 sprzężenia z ładunkiem

W przykładach do tej pory tabela sprzężenia została użyta tylko do przechowywania par kluczy obcych reprezentujących każde skojarzenie. 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 jednostki sprzężenia i dodać właściwości "ładunek skojarzenia" do tego typu. Często można również tworzyć nawigacje do jednostki sprzężenia oprócz "pomijania nawigacji" używanego dla relacji 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. Na 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. Na 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 jednostki z ustawionym znacznikiem czasu automatycznie po wstawieniu 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);

Napiwek

Pokazany tutaj język SQL jest przeznaczony dla sqlite. W programie SQL Server/usłudze Azure SQL użyj polecenia .HasDefaultValueSql("GETUTCDATE()") i do TEXT odczytu datetime.

Niestandardowy typ jednostki typu udostępnionego jako jednostka sprzężenia

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 sprzężenia mają kolumnę CreatedOn . Można je mapować przy użyciu JoinType klasy mapowanej jako typ jednostki typu współużytkowanego:

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

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 jednostki sprzężenia są ograniczone do kluczy podstawowych typów jednostek po obu stronach 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(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

Ponadto wynikowy schemat bazy danych, aby uzyskać jasność, w tym 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. Na 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>(
            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));
}

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

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. Na 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>();
}

Wynikowy schemat tabeli sprzężenia 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 z typami just 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ą sprzężenia i dwiema stronami relacji wiele-do-wielu są tworzone przy użyciu kaskadowego zachowania 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. Na przykład:

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

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". Na przykład:

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

Spowoduje to mapowanie tabeli sprzężenia o nazwie PersonPerson, przy użyciu obu kluczy obcych wskazujących People z powrotem do tabeli:

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łom z osobą B, a następnie 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. Na 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. Na przykład:

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

Bezpośrednie użycie tabeli sprzężenia

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

  • Sesja standup społeczności danych platformy .NET z głębokim omówieniem wielu do wielu i infrastruktury będącej jej podstawą.