EF Core 5 relacja wiele do wielu

Wprowadzenie

Jedną z nowości, jaka pojawiła się w Entity Framework Core 5 jest obsługa relacji wiele do wielu bez konieczności dodawania klasy dla tabeli łączącej. Trochę to zajęło, szczególnie, że było to dostępne w zwykłym Entity Framework. Jednak, co istotne, systematycznie są dodawane kolejne rzeczy do Entity Framework Core. Bardzo fajne jest to, że z relacji możemy korzystać na dwa sposoby, o czym w tym wpisie 🙂

Relacja wiele do wielu

Relacje wiele do wielu realizuje się za pomocą dodatkowej tabeli łączącej, w której na ogół znajdują się dwie kolumny z identyfikatorami rekordów, które łączymy. Co widać poniżej, gdzie mamy tabelę Employees dla pracowników, Projects dla projektów oraz EmployeeProject, która jest właśnie tabelą łączącą.

Relacja wiele do wielu na poziomie bazy danych

Część ORMów ma wsparcie dla relacji wiele do wielu, dzięki czemu nie musimy tworzyć dodatkowej klasy łączącej, które będzie odpowiadała tabeli łączącej. W modelu obiektowym mamy dwie klasy, w przykładzie Employee oraz Project, które następnie mają właściwości typu ICollection do drugiej klasy. Sam ORM tłumaczy wszystko na tę dodatkową tabele łączącą.

Relacja wiele do wielu na poziomie klas

Entity Framework Core od wersji 5 dołączył do grona ORMów, które zaczęły wspierać relacje wiele do wielu. W wcześniejszych wersjach niestety musieliśmy dodawać klasę łączącą.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Wiele do wielu w EF Core 5

Relacja wiele do wielu w przypadku Entity Framework Core 5 jest dość łatwa do realizacji. Gdy chcemy połączyć dwie klasy tego typu relacją, to wystarczy tylko dodać właściwość typu ICollection<typ> w drugiej klasie, co najlepiej widać na poniższym przykładzie, w którym łączymy klasę pracownika z klasą projektu. Pracownik może pracować nad kilkoma projektami oraz projekt może być realizowany przez kilku pracowników:

public class Employee
{
public Guid Id { get; set; } = Guid.NewGuid();
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual ICollection<Project> Projects { get; set; } = new List<Project>();
}
view raw Employee.cs hosted with ❤ by GitHub
public class Project
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
public virtual ICollection<Employee> Employees { get; set; } = new List<Employee>();
}
view raw Project.cs hosted with ❤ by GitHub

Właściwości, o których mowa, są odpowiednio w linijce 9 klasy Employee oraz w linijce 7 klasy Project. W tym momencie nie jest potrzebna żadna dodatkowa konfiguracja, możemy bazować na domyślnych konwencjach.

Dla porządku jeszcze zawartość klasy DataContext (w przykładzie używam bazy Sqlite, aby łatwiej można było uruchomić projekt po jego pobraniu z githuba):

public class DataContext : DbContext
{
public DataContext()
{
}
public DataContext(DbContextOptions options) : base(options)
{
}
public DbSet<Project> Projects { get; set; }
public DbSet<Employee> Employees { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename=EFCoreManyToMany.db", options =>
{
options.MigrationsAssembly(this.GetType().Assembly.FullName);
});
base.OnConfiguring(optionsBuilder);
}
}
view raw DataContext.cs hosted with ❤ by GitHub

W efekcie wygenerowana migracja wygląda tak (jest w nich również tworzenie samych tabel Employees oraz Projects):

public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Employees",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
FirstName = table.Column<string>(type: "TEXT", nullable: true),
LastName = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Employees", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Projects",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Projects", x => x.Id);
});
migrationBuilder.CreateTable(
name: "EmployeeProject",
columns: table => new
{
EmployeesId = table.Column<Guid>(type: "TEXT", nullable: false),
ProjectsId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeeProject", x => new { x.EmployeesId, x.ProjectsId });
table.ForeignKey(
name: "FK_EmployeeProject_Employees_EmployeesId",
column: x => x.EmployeesId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EmployeeProject_Projects_ProjectsId",
column: x => x.ProjectsId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EmployeeProject_ProjectsId",
table: "EmployeeProject",
column: "ProjectsId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EmployeeProject");
migrationBuilder.DropTable(
name: "Employees");
migrationBuilder.DropTable(
name: "Projects");
}
}
view raw Initial.cs hosted with ❤ by GitHub

Jak widać migracja tworzy trzy tabele, z czego ostatnia EmployeeProject jest właśnie tą tabelą łączącą. Zawiera one dwie kolumny (EmployeesId oraz ProjectsId), które są kluczami obcymi oraz razem tworzą klucz główny tabeli.

Przykład działania relacji wiele do wielu

Samo użycie takiej relacji jest dość proste i Entity Framework Core uwzględni za nas istnienie tabeli łączącej. Na przykład dodanie obiektów do bazy wygląda tak:

private static async Task AddData()
{
using (var db = new DataContext())
{
var project1 = new Project()
{
Name = "project 1"
};
await db.Projects.AddAsync(project1);
var project2 = new Project()
{
Name = "project 2"
};
await db.Projects.AddAsync(project2);
var employee = new Employee()
{
FirstName = "Daniel",
LastName = "Plawgo",
Projects = new List<Project>()
{
project1,
project2
}
};
await db.Employees.AddAsync(employee);
await db.SaveChangesAsync();
}
}
view raw AddData.cs hosted with ❤ by GitHub

Powyższy fragment kodu tworzy dwa projekty i dodaje je do bazy. Następnie tworzy pracownika i przypisuje do niego wcześniej utworzone projekty. Tutaj po prostu dodajemy te projekty do kolekcji Projects w klasie pracownika.

W efekcie na bazie danych wykonają się takie zapytania (poniżej jest log z Entity Framework Core, z którego usunąłem zbędne rzeczy):

INSERT INTO "Employees" ("Id", "FirstName", "LastName")
VALUES (@p0, @p1, @p2);
INSERT INTO "Projects" ("Id", "Name")
VALUES (@p0, @p1);
INSERT INTO "Projects" ("Id", "Name")
VALUES (@p0, @p1);
INSERT INTO "EmployeeProject" ("EmployeesId", "ProjectsId")
VALUES (@p2, @p3);
INSERT INTO "EmployeeProject" ("EmployeesId", "ProjectsId")
VALUES (@p0, @p1);
view raw AddData.log hosted with ❤ by GitHub

Jak widać Entity Framework Core sam obsłużył tabelę łączącą.

Podobny efekt jest w momencie pobierania danych z bazy:

private static async Task ShowData()
{
using (var db = new DataContext())
{
var employee = await db.Employees
.Include(e => e.Projects)
.FirstOrDefaultAsync();
Console.WriteLine($"{employee.FirstName} {employee.LastName}:");
foreach (var project in employee.Projects)
{
Console.WriteLine($"\t{project.Name}");
}
}
}
view raw ShowData.cs hosted with ❤ by GitHub

Powyższy fragment kodu pobiera pierwszego pracownika (dodanego wcześniej) wraz z jego projektami (wywołanie metody Include). W efekcie na bazie danych wykona się takie zapytanie:

SELECT "t"."Id", "t"."FirstName", "t"."LastName", "t0"."EmployeesId", "t0"."ProjectsId", "t0"."Id", "t0"."Name"
FROM (
SELECT "e"."Id", "e"."FirstName", "e"."LastName"
FROM "Employees" AS "e"
LIMIT 1
) AS "t"
LEFT JOIN (
SELECT "e0"."EmployeesId", "e0"."ProjectsId", "p"."Id", "p"."Name"
FROM "EmployeeProject" AS "e0"
INNER JOIN "Projects" AS "p" ON "e0"."ProjectsId" = "p"."Id"
) AS "t0" ON "t"."Id" = "t0"."EmployeesId"
ORDER BY "t"."Id", "t0"."EmployeesId", "t0"."ProjectsId", "t0"."Id"
view raw ShowData.log hosted with ❤ by GitHub

Tutaj też dobrze widać obsługę tabeli łączącej (linijki 8-10) i nie musimy tego robić sami.

Dodanie klasy łączącej

A co w sytuacji, gdy jednak z czasem będziemy potrzebowali dodać jakieś dodatkowe dane w relacji? Na przykład potrzebujemy określić rolę użytkownika w projekcie. Czyli chcielibyśmy dodać kolumnę Role do EmployeeProject (dla uproszczenia przykładu dodam tekstową kolumnę zamiast klucza do tabeli z rolami).

W Entity Framework Core 5 fajne jest to, że możemy dodatkowo dodać typ dla tabeli łączącej obok już istniejącej relacji. Dzięki czemu nie będziemy musieli usuwać starej relacji z kodu, a co za tym idzie modyfikować kod w wielu miejscach. Możemy równolegle korzystać z jednego i drugiego.

Aby to zrealizować na początku musimy dodać nową klasę dla tabeli łączącej:

public class EmployeeProject
{
public Guid EmployeesId { get; set; }
public virtual Employee Employee { get; set; }
public Guid ProjectsId { get; set; }
public virtual Project Project { get; set; }
public string Role { get; set; }
}
view raw EmployeeProject.cs hosted with ❤ by GitHub

W niej mamy po dwie właściwości (jedna dla klucza, druga dla obiektu) dla każdego końca relacji oraz dodatkową właściwość dla informacji o roli użytkownika (wspomniana wcześniej kolumna tekstowa Role).

Następnie w klasie Employee oraz Project dodajemy drugą kolekcję, która teraz będzie przechowywać obiekty EmployeeProject. Zwróć uwagę, że wcześniejsze kolekcje działają i dalej możemy z nich korzystać.

public class Employee
{
public Guid Id { get; set; } = Guid.NewGuid();
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual ICollection<Project> Projects { get; set; } = new List<Project>();
public virtual ICollection<EmployeeProject> EmployeeProjects { get; set; } = new List<EmployeeProject>();
}
view raw Employee2.cs hosted with ❤ by GitHub
public class Project
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; }
public virtual ICollection<Employee> Employees { get; set; } = new List<Employee>();
public virtual ICollection<EmployeeProject> EmployeeProjects { get; set; } = new List<EmployeeProject>();
}
view raw Project2.cs hosted with ❤ by GitHub

Na końcu zostaje nam jeszcze skonfigurować w Entity Framework Core typ dla klasy łączącej. Na listingu poniżej wrzuciłem tylko zawartość metody OnModelCreating z klasy DataContext (w realnym projekcie użyłbym klasy EntityTypeConfiguration), pozostała jej część jest taka sama jak wyżej (całość również znajdziesz w githubie):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>()
.HasMany(p => p.Employees)
.WithMany(p => p.Projects)
.UsingEntity<EmployeeProject>(
j => j
.HasOne(pt => pt.Employee)
.WithMany(t => t.EmployeeProjects)
.HasForeignKey(pt => pt.EmployeesId),
j => j
.HasOne(pt => pt.Project)
.WithMany(p => p.EmployeeProjects)
.HasForeignKey(pt => pt.ProjectsId),
j =>
{
j.Property(pt => pt.Role).HasDefaultValueSql("'employee'");
j.HasKey(t => new { t.EmployeesId, t.ProjectsId });
});
}
view raw OnModelCreating.cs hosted with ❤ by GitHub

W powyższym fragmencie kodu konfigurujemy naszą relację wiele do wielu, gdzie kluczowe jest wywołanie metody UsingEntity (linijka 6). W niej właśnie określamy użycie dodatkowej klasy EmployeeProject i zależności między właściwościami.

Dodatkowo ostatni fragment jest również ważny, w szczególności linijka 17. W niej konfigurujemy domyślną wartość dla kolumny Role. Jest to o tyle istotne, że dalej możemy korzystać z wcześniejszych kolekcji, w których nie będziemy mogli ustawić wartości dla roli, wtedy będzie użyta właśnie ta wartość domyślna.

Co fajne, w tym momencie możemy korzystać ze starych właściwości z pierwszej wersji relacji oraz nowych z dodatkowym typem dla tabeli łączącej.

Działanie relacji z typem dla tabeli łączącej

Na początku zobaczmy, jak skorzystać z nowej kolekcji. Robimy to w identyczny sposób, jak byśmy zrobili, mając od razu relacje wiele do wielu z typem dla tabeli łączącej.

Dodanie nowego projektu dla użytkownika z rolą Owner może wyglądać tak:

private static async Task AddDataWithRole()
{
using (var db = new DataContext())
{
var project3 = new Project()
{
Name = "project 3"
};
await db.Projects.AddAsync(project3);
var employee = await db.Employees
.FirstOrDefaultAsync();
var employeeProject = new EmployeeProject()
{
Project = project3,
Employee = employee,
Role = "Owner"
};
employee.EmployeeProjects.Add(employeeProject);
await db.SaveChangesAsync();
}
}
view raw AddDataWithRole.cs hosted with ❤ by GitHub

Czyli pobieramy obiekt pracownika, tworzymy nową instancję klasy EmployeeProject i dodajemy do kolekcji EmployeeProjects. Na końcu wszystko zapisujemy.

Pobranie danych i ich wyświetlenie jest podobne jak wcześniej, tylko że ponownie korzystamy z kolekcji EmployeeProjects. Tym razem musimy wykonać jeszcze dodatkowego include, aby pobrać projekty:

private static async Task ShowDataWithRole()
{
using (var db = new DataContext())
{
var employee = await db.Employees
.Include(e => e.EmployeeProjects)
.ThenInclude(p => p.Project)
.FirstOrDefaultAsync();
Console.WriteLine($"{employee.FirstName} {employee.LastName}:");
foreach (var item in employee.EmployeeProjects)
{
Console.WriteLine($"\t{item.Project.Name} - {item.Role}");
}
}
}
view raw ShowDataWithRole.cs hosted with ❤ by GitHub

W obu sytuacjach (korzystanie z właściwości Project oraz EmployeeProjects) powoduje to wykonanie na bazie danych tego samego zapytania. Do tego Entity Framework Core 5 po wykonaniu zapytania uzupełnia dane w obu właściwościach.

Przykład

Przykład do tego wpisu znajduje się na githubie (https://github.com/danielplawgo/EFCoreManyToMany). Po jego pobraniu możesz go od razu uruchomić. Aplikacja wykorzystuje Sqlite do przechowywania danych.

Podsumowanie

Fajnie, że Microsoft uzupełnia braki w Entity Framework Core w stosunku do starego Entity Framework. Co prawda w ostatnim czasie rzadko używałem relacji wielu do wielu bez klasy dla tabeli łączącej. Ale warto wiedzieć, że w Entity Framework Core można łatwo z czasem dodać klasę dla tabeli łączącej i dodać tam dodatkowe właściwości.

A co Ty o tym myślisz?

Szkolenie Entity Framework Core

Szkolenie Entity Framework Core

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie z Entity Framework Core.

4 thoughts on “EF Core 5 relacja wiele do wielu

  • Pingback: dotnetomaniak.pl
    • Hej Krzysztof!

      Faktycznie wkradł tam się drobny błąd, ale w tym przypadku to on nie ma znaczenia. W tym przypadku Entity Framework Core nie doda drugi raz tego samego obiektu do bazy, bo metoda AddAsync oznacza go do dodania, natomiast dodaje się on fizycznie podczas zapisu w SaveChangesAsync.

      Natomiast project2 dodaje się tutaj w sposób niejawny poprzez dodanie employee. Entity Framework dodaje również obiekty powiązane podczas dodawania jakiegoś obiektu. Czyli tutaj dodając obiekt employee, dodawany jest również i project2. Więc w tym przypadku tak naprawdę dodanie projektów jest zbędne, zostałyby on i tak dodane podczas dodawania pracownika.

      Błąd poprawiłem, dzięki za zwrócenie uwagi!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *