Wprowadzenie

W jednym z poprzednich wpisów poruszaliśmy zagadnienie automatycznej zmiany EntityState w scenariuszu połączonym. Tym razem skupimy się na zachowaniach encji głównej i podrzędnej w scenariuszu rozłączonym.

Prześledzimy zmiany związane z dołączeniem encji do kontekstu oraz wpływem udostępnionych metod na zmianę EntityState. Do naszej dyspozycji zostały oddane poniższe metody:

  • Attach()
  • Entry()
  • Add()
  • Update()
  • Remove()

Attach()

Metoda Attach() dostępna w obrębie kontekstu (DbContext.Attach()) oraz encji (DbSet.Attach()) pozwala na dołączenie odłączonej encji do kontekstu i rozpoczęcie śledzenia zmian. Wykorzystując wiedzę zdobytą we wpisie EF Core - ChangeTracker nieco skomplikujemy nasz przykład. Nasza główna encja będzie posiadała referencję do dwóch innych encji dzięki czemu będziemy mogli swobodniej operować kluczami tak, aby wygenerować różne stany EntityState.

Nasza bazowa definicja relacji przyjmuje poniższą postać:

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

	public string FullName { get; set; }

	public Address Address { get; set; }

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

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

	public string City { get; set; }

	public string CustomerAddress { get; set; }

    public int CustomerId { get; set; }
	public Customer Customer { 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; }
}

Na bazie modelu prześledzimy zachowanie metody Attach() na stan EntityState dla każdej encji w naszej hierarchi:

var customer = new Customer()
{
	FullName = "Paweł",
	Address = new Address()
	{
		Id = 1,
		City = "Gdynia",
		CustomerAddress = "Port Gdynia"
	},
	Orders = new List<Order>()
	{
		new Order() { OrderName = "Samochód"},
		new Order() { Id = 2}
	}
};
			
using(var context = new ApplicationDbContext())
{
	context.Attach(customer).State = EntityState.Added;

	CheckEntityState(context.ChangeTracker.Entries());
}
Wynik działania przedstawia się w poniższy sposób:
// Encja: Customer, EntityState: Added
// Encja: Order, EntityState: Added
// Encja: Address, EntityState: Unchanged
// Encja: Order, EntityState: Unchanged

W powyższym przykładzie customer jest instancją encji Customer, która zawiera właściwości nawigacyjne do encji Address w relacji jeden do jednego oraz encji Order w relacji jeden do wielu. Wykorzystując metodę Attach(customer).State = EntityState.Added dołączamy odłączoną encję (jako całość) do naszego kontekstu ustawiając stan na Added - pierwsza linia wyniku działania.

W przypadku encji głównej (Customer) nie ma znaczenia czy zawiera ona wartość klucza głównego – ten krok to dołączenie odłączonej encji z odpowiednim stanem i rozpoczęcie procesu śledzenia. Sytuacja wygląda inaczej w przypadku encji podrzędnych. W pierwszej kolejności zwróćcie uwagę na adres z identyfikatorem Id = 1 - korzystamy z wpisu dostępnego w bazie danych dlatego EntityState został oznaczony jako Unchanged. Podobną sytuację możemy zobaczyć na zleceniu z identyfikatorem Id = 2. Sytuacja wygląda zupełnie inaczej dla zlecenia, którego nie było jeszcze w naszej bazie danych - EntityState został ustawiony na Added a po wywołaniu metody SaveChanges() rekord ten zostanie dodany do tabeli.

Oczywiście...jeżeli spróbujemy przypisać do naszej encji głównej adres (lub zamówienie) na bazie identyfikatora, którego nie mamy w bazie danych zostanie rzucony wyjątek. Spójrzcie dlatego na poniższą tabelę, która przedstawia zachowanie metody Attach w zależności od różnych wartości EntityState dla odłączonej jednostki:

Metoda: Attach() Encja główna z wartością klucza Encja główna z pustą lub domyślną wartością Encja podrzędna z wartością klucza Encja podrzędna z pustą lub domyślną wartością
EntityState=Added Added Added Unchanged Added
EntityState=Modified Modified Wyjątek! Unchanged Added
EntityState=Deleted Deleted Wyjątek! Unchanged Added

Entry()

Jeżeli wcześniej korzystaliście z EF 6.x musicie mieć świadomość, że metoda DbContext.Entry() zachowuje się nieco inaczej w Entity Framework Core. Analizę przeprowadzimy na poniższym przykładzie:

// Brak klucza encji głównej
var customer = new Customer()
{
	FullName = "Paweł",
	Address = new Address() // encja podrzędna z wartością klucza
	{
		Id = 1,
		City = "Gdynia",
		CustomerAddress = "Port Gdynia"
	},
	Orders = new List<Order>()
	{
		new Order() { OrderName = "Samochód"}, // encja podrzędna bez klucza
		new Order() { Id = 2} // encja podrzędna z wartością klucza
	}
};
			
using(var context = new ApplicationDbContext())
{
	context.Entry(customer).State = EntityState.Modified;

	CheckEntityState(context.ChangeTracker.Entries());
}
Wynik działania:
// Encja: Customer, EntityState: Modified

W powyższym przykładzie metoda Entry() dołącza encję do kontekstu ustawiając EntityState na Modified. Podobnie jak w poprzednim wypadku nie ma znaczenia czy encja główna posiada czy nie wartość klucza. Wszystkie podrzędne jednostki są ignorowane, nie są dołączane ani nie dochodzi do ustawienia ich stanu EntityState.

Spójrzcie jeszcze na tabelę obrazującą zachowanie metody Entry() w zależności od różnych stanów:

Metoda: Entry() Encja główna z wartością klucza Encja główna z pustą lub domyślną wartością Encja podrzędna z/bez wartości klucza
EntityState=Added Added Added Nie brany pod uwagę!
EntityState=Modified Modified Modified Nie brany pod uwagę!
EntityState=Deleted Deleted Deleted Nie brany pod uwagę!

Add()

Metoda Add() dostępna w ramach kontekstu jak i encji dołącza odłączoną encję do kontekstu ustawiając parametr EntityState na Added dla elementu głównego oraz podrzędnych niezależnie od tego czy mają wartości kluczy czy nie. Spójrzcie na poniższy przykład:

// Brak klucza encji głównej
var customer = new Customer()
{
	FullName = "Paweł",
	Address = new Address() // encja podrzędna z wartością klucza
	{
		Id = 1,
		City = "Gdynia",
		CustomerAddress = "Port Gdynia"
	},
	Orders = new List<Order>()
	{
		new Order() { OrderName = "Samochód"}, // encja podrzędna bez klucza
		new Order() { Id = 2} // encja podrzędna z wartością klucza
	}
};
			
using(var context = new ApplicationDbContext())
{
	context.Customer.Add(customer);

	CheckEntityState(context.ChangeTracker.Entries());
}
Wynik działania:
// Encja: Customer, EntityState: Added
// Encja: Address, EntityState: Added
// Encja: Order, EntityState: Added
// Encja: Order, EntityState: Added

W tym wypadku nie potrzebujemy dodatkowej tabeli – stan, niezależnie od wartości/lub braku klucza, zawsze zostaje ustawiony na Added.

Update()

Metoda Update() dostępna w obrębie kontekstu oraz encji dołącza odłączoną encje do kontekstu i ustawia wartość EntityState każdej jednostki w zależności od wartości/braku klucza. Spójrzcie poniżej:

// Brak klucza encji głównej
var customer = new Customer()
{
	FullName = "Paweł",
	Address = new Address() // encja podrzędna z wartością klucza
	{
		Id = 1,
		City = "Gdynia",
		CustomerAddress = "Port Gdynia"
	},
	Orders = new List<Order>()
	{
		new Order() { OrderName = "Samochód"}, // encja podrzędna bez klucza
		new Order() { Id = 2} // encja podrzędna z wartością klucza
	}
};
			
using(var context = new ApplicationDbContext())
{
	context.Update(customer);

	CheckEntityState(context.ChangeTracker.Entries());
}
Wynik działania:
// Encja: Customer, EntityState: Added
// Encja: Order, EntityState: Added
// Encja: Address, EntityState: Modified
// Encja: Order, EntityState: Modified

W powyższym przykładzie metoda Update() ustawia stan encji na Modified w przypadku niepustej wartości klucza. Wartości puste lub domyślne (niezależnie czy mówimy o encji głównej czy podrzędnej) powodują ustawienie stanu na Added.

Remove

Jak doskonale pamiętacie z poprzednich wpisów operacja kasowania danych nie jest tak prosta jak nam się wydaje – wszystko zależy od posiadanej wiedzy i poprawnego jej wykorzystania. Sposób kasowania danych połączonych możemy zdefiniować używając Fluent API. Wówczas zdecydujemy co stanie się z encjami połączonymi kiedy wykorzystamy metodę Remove().

W przypadku tego wpisu użyliśmy konwencji. Domyślnym zachowaniem dla takiego podejścia jest skasowanie wszystkich połączonych encji w myśl typu wyliczeniowego DeleteBehavior.Cascade. Sprawdźmy jaki będzie efekt wykorzystania wspomnianej metody:

// Kasując dane musimy pamiętać o identyfikatorze
var customer = new Customer()
{
	Id = 1,
	FullName = "Paweł",
	Address = new Address() // encja podrzędna z wartością klucza
	{
		Id = 1,
		City = "Gdynia",
		CustomerAddress = "Port Gdynia"
	},
	Orders = new List<Order>()
	{
		new Order() { OrderName = "Samochód"}, // encja podrzędna bez klucza
		new Order() { Id = 2} // encja podrzędna z wartością klucza
	}
};
			
using(var context = new ApplicationDbContext())
{
	context.Remove(customer);

	CheckEntityState(context.ChangeTracker.Entries());
}
Wynik działania:
// Encja: Customer, EntityState: Deleted
// Encja: Address, EntityState: Deleted
// Encja: Order, EntityState: Deleted

W tym momencie musimy pamiętać, że próba wykasowania encji bez zdefiniowanego klucza spowoduje rzucenie poniższego wyjątku:

System.InvalidOperationException: 'The property 'Customer.Id' has a temporary value while attempting to change the entity's state to 'Deleted'. Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property.'

W kolejnym wpisie poruszymy ciekawe zagadnienie metody TrackGraph() dostępnej w klasie ChangeTracker, która pozwoli nam na ręczne ustawienie odpowiedniego EntityState dla każdej z encji.