Wprowadzenie

W jednym z poprzednich wpisów nauczyliśmy się tworzenia relacji jeden do wielu korzystając z domyślnych konwencji (EF Core - relacja jeden do wielu). Pojawia się zatem pytanie: Dlaczego mielibyśmy używać konfiguracji przy użyciu Fluent API skoro EF Core posiada wbudowane konwencje do tworzenia tego typu relacji? Odpowiedź jest prosta: takie podejście będzie łatwiejsze z perspektywy przyszłego zarządzania projektem.

Spójrzmy na poniższy przykład dwóch klas: Customer oraz Order. Tworząc relację jeden do wielu będziemy w stanie utworzyć wiele różnych zamówień zrealizowanych przez danego Klienta:

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

	public string FullName { get; set; }

	public ICollection<Order> Orders { get; set; }
}

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

	public string OrderName { get; set; }

	public int CustomerId { get; set; }
	public Customer Customer { get; set; }
}

Mając w pamięci poprzedni wpis: konfiguracji dokonamy nadpisując metodę OnModelCreating zgodnie z poniższym przykładem:

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

	public DbSet<Order> Orders { get; set; }

	public ApplicationDbContext()
	{

	}

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

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		// W tym miejscu możemy przygotować konfigurację
		// wykorzystując Fluent API

		// Konfiguracja relacji jeden do wielu
		modelBuilder.Entity<Order>()
			.HasOne<Customer>(c => c.Customer)
			.WithMany(o => o.Orders)
			.HasForeignKey(c => c.CustomerId);
	}
}

Zanim przejdziemy przez proces migracji i utworzenie schematu po stronie bazy danych postarajmy się zrozumieć powyższy kod:

  1. W pierwszym kroku rozpoczynamy nasz proces konfiguracyjny – możemy rozpocząć od dowolnej klasy, tj. Customer lub Order. W naszy przypadku rozpoczynamy od klasy Order:
    modelBuilder.Entity<Order>()
    
  2. Następnie wykorzystując metodę HasOne określamy, że encja Order zawiera właściwość typu Customer o nazwie Customer:
    .HasOne<Customer>(c => c.Customer)
    
  3. Kolejny krok to konfiguracja drugiego końca relacji, tj. tabeli naszych Klientów. Dzięki metodzie WithMany określamy, że encja Customer zawiera wiele encji Order:
    .WithMany(c => c.Orders)
    
  4. Brakuje nam jeszcze klucza obcego. Ten, zgodnie z konwencjami, może ale nie musi być zdefiniowany – ja preferuje takie podejście, tzn. pełnej definicji, z uwagi na czytelność kodu. Jeżeli posługujemy się pełną defincją wykorzystamy metodę HasForeignKey w celu określenia nazwy klucza obcego:
    .HasForeignKey(o => o.CustomerId);
    

Mam nadzieję, że wszystko jest jasne. Wykorzystajmy teraz dostępne polecenia migracji, tj. add-migration oraz update-database i sprawdźmy czy nasze relacje zostały poprawnie wygenerowane: EFCore: konfiguracja relacji jeden do wielu przy wykorzystaniu Fluent API.

Jak doskonale widać została utworzona relacja jeden do wielu. Dany Klient może złożyć wiele różnych zamówień na produkty dostępne w naszym sklepie.

Nieco wcześniej wspomniałem, że konfigurację możemy rozpocząć korzystając z dowolnej strony relacji. Tym razem rozpoczniemy od encji Customer odwracając nieco zależności:

modelBuilder.Entity<Customer>()
	.HasMany<Order>(o => o.Orders)
	.WithOne(c => c.Customer)
	.HasForeignKey(c => c.CustomerId);

Automatyczne usuwanie powiązanych wierszy

Automatyczne usuwanie powiązanych wierszy skonfigurowane przy pomocy Fluent API może pozwolić nam na wyeliminowanie wielu problemów. Wyobraźmy sobię sytuację w której usuwamy dany produkt z bazy danych – możemy również usunać z bazy danych wszystkie zamówienia, które zostały złożone przez danych Klientów na nasz produkt (nie jesteśmy wielką platformą na której dokonujemy zakupów, produkt jest starszy niż np. 10 lat, nie jest i nigdy już nie będzie ofertowany a liczba zamówień przekroczyła kilka milionów co znacząco wpływa na wydajność działania bazy danych…).

Z pomocą w takiej sytuacji przychodzi nam metoda OnDelete, która pozwala na kaskadowe usuwanie danych pomiędzy encjami Customer oraz Order:

modelBuilder.Entity<Order>()
	.HasOne<Customer>(c => c.Customer)
	.WithMany(o => o.Orders)
	.HasForeignKey(c => c.CustomerId);
    .OnDelete(DeleteBehavior.Cascade);

Metoda OnDelete wykorzystuje parameter DeleteBehavior, który definiuje sposób usuwania danych:

  • ClientSetNull - wartości kluczy obcych w encjach zależnych zostaną ustawione na null;
  • Restrict - jednostki zależne nie zostaną usunięte;
  • SetNull - wartości kluczy obcych w encjach zależnych zostaną ustawione na null;
  • Cascade - encje zależne zostaną usunięte po usunięciu encji głównej;
  • ClientCascade - encje zależne zostaną usunięte po usunięciu encji głównej. Działanie zostanie wykonane przez EF nawet jeżeli silnik danej bazy danej nie wspiera tej operacji;
  • NoAction - wartości kluczy obcych w encjach zależnych zostaną ustawione na null (zachowanie stanu spójności podczas śledzenia zmian);
  • ClientNoAction - (parametr zwykle nieużywany) wartości kluczy obcych w encjach zależnych nie zostaną ustawione na null co może spowodować niespójność wykresu encji (o nim porozwiamy w jednym z kolejnych wpisów).