Wprowadzenie

W tym wpisie poznamy sposób modelowania relacji wiele do wielu za pomocą Fluent API. Nasz przykład będzie bazował na relacjach pomiędzy pracownikami i projektami. W ramach różnych obowiązków dany pracownik może zostać przypisany do wielu projektów. Jednocześnie do danego projektu może być przypisanych wielu różnych pracowników. Dzięki takiemu podejściu zaimplementujemy relację wiele do wielu.

Nasze bazowe modele przedstawiają się w poniższy sposób:

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

	public string FullName { get; set; }
}

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

	public string ProjectName { get; set; }
}

W przypadku modelowania relacji wiele do wielu musimy pamiętać, że relacja po stronie bazy danych reprezentowana jest przez tabelę łączącą, która zawiera klucze obce obu tabel. Jednocześnie, wspomniane klucze obce, są złożonym kluczem głównym. Spójrzcie na poniższy diagram, który jest naszym docelowym efektem modelowania relacji: Fluent API: relacja wiele do wielu

Fluent API

Teraz już chyba doskonale wiecie dlaczego nie dokonałem modelowania relacji wiele do wielu przy wykorzystaniu domyślych konwencji – nie zostały one zdefiniowane dla tego typu relacji. Jedyną drogą jest wykorzystanie Fluent API (o ile nie dokonaliśmy przejścia na EF Core 5 - o tym jednak w podsumowaniu).

Zanim jednak przejdziemy do konfiguracji musimy utworzyć model (klasę) łączącą obie encje. Będzie ona zawierała właściwości kluczy obcych oraz referencyjne właściwości nawigacyjne dla każdej encji.

Krok 1: tworzymy nowy model łączący obie encje – definiujemy właściwości dla kluczy obcych oraz referencyjną właściwość nawigacyjną dla każdej jednostki:

public class EmployeeProject
{
    // klucz obcy właściwości nawigacyjnej Employee
	public int EmployeeId { get; set; }

    // Właściwość nawigacyjna encji Employee
	public Employee Employee { get; set; }


    // klucz obcy właściwości nawigacyjnej Project
	public int ProjectId { get; set; }

    // Właściwość nawigacyjna encji Project
	public Project Project { get; set; }
}

Krok 2: definiujemy relację wiele do wielu pomiędzy bazowymi encjami poprzez dostanie właściwości nawigacyjnych dla obu encji: Employee oraz Project:

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

	public string FullName { get; set; }

	public IList<EmployeeProject> EmployeeProject { get; set; }
}

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

	public string ProjectName { get; set; }

	public IList<EmployeeProject> EmployeeProject{ get; set; }
}
Zwróćcie uwagę, że posługujemy się encją EmployeeProject. W swojej definicji zawiera klucze obce oraz właściwości nawigacyjne zarówno dla pracownika jak i projektu. Takie połączenie pozwala na definicję relacji jeden do wielu.

Krok 3: konfiguracja kluczy obcych w encji łączącej jako klucz złożony przy wykorzystaniu Fluent API:

public class ApplicationDbContext : DbContext
{
	public DbSet<Employee> Employee { get; set; }

	public DbSet<Project> Project { get; set; }

	public DbSet<EmployeeProject> EmployeeProject { get; set; }

	public ApplicationDbContext()
	{

	}

	protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
	{
		if (!optionBuilder.IsConfigured)
		{
			optionBuilder
				.UseSqlServer(@"Server=PAWEL;database=EFCoreFluentAPIManyToMany;Integrated Security=true;MultipleActiveResultSets=true").EnableSensitiveDataLogging(true);
		}
	}

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		// Konfiguracja złożonego klucza głównego w tabeli łączącej obie encje
		modelBuilder.Entity<EmployeeProject>().HasKey(ep => new { ep.EmployeeId, ep.ProjectId });
	}
}

W powyższy sposób możemy skonfigurować relację wiele do wielu o ile spełnione są domyślne konwencję dotyczące nazewnictwa pól. Jeżeli te konwencje nie będą przestrzegane, np. zamiast pola EmployeeId zdefiniujemy EID oraz pole ProjectId przyjmie postać PID musimy dodatkowo posłużyć się konfiguracją Fluent API tak jak pokazano poniżej:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	// Konfiguracja złożonego klucza głównego w tabeli łączącej obie encje
	modelBuilder.Entity<EmployeeProject>().HasKey(ep => new { ep.EmployeeId, ep.ProjectId });


	modelBuilder.Entity<EmployeeProject>()
		.HasOne<Employee>(e => e.Employee)
		.WithMany(p => p.EmployeeProject)
		.HasForeignKey(e => e.EID);

	modelBuilder.Entity<EmployeeProject>()
		.HasOne<Project>(p => p.Project)
		.WithMany(e => e.EmployeeProject)
		.HasForeignKey(p => p.PID);
}

Konfiguracja jest już za nami. Dokonajmy migracji celem sprawdzenia wygenerowanego schematu bazy danych: Fluent API: relacja wiele do wielu

Udało nam się zamodelować relację wiele do wielu wykorzystując tabele łączącą oraz wykorzystując konfigurację Fluent API. Poniżej możemy jeszcze raz spojrzeć na diagram naszej bazy danych: Fluent API: relacja wiele do wielu

Podsumowanie

Dlaczego zdecydowałem się na taki sposób zamodelowania relacji wiele do wielu? Wszystko dlatego, że nie każdy od razu przesiada się na najnowsze wersje frameworka. Jawne tworzenie tabeli łączącej obie encje jest wymagane do zamodelowania relacji wiele do wielu. Jednakże... wraz z pojawieniem się EF Core 5 dostaliśmy bezpośredni sposób modelowania takiej relacji. Osoby zainteresowane odysłam do nagrania: Entity Framework Community Standup - August 19th 2020 - Many-to-Many in EF Core 5.0

Od teraz możemy dokonać prostego modelowania w poniższej postaci (bez żadnych dodatkowych konfiguracji):

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

	public string FullName { get; set; }

	public ICollection<Project> Projects { get; } = new List<Project>();
}

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

	public string ProjectName { get; set; }

	public ICollection<Employee> AssignedProjects { get; } = new List<Employee>();
}
Spójrzcie na poniższy diagram bazy danych (specjalnie zmieniłem nazwy, żeby każdy z Was mógł zobaczyć, że ta funkcjonalność jest dostępna i działa poprawnie): Fluent API: relacja wiele do wielu