Wprowadzenie
Do tej pory wszystkie operacje wykonywaliśmy w obrębie zdefiniowanego kontekstu. DbContext był w pełni świadomy wszelkich modyfikacji, dodawania czy kasowania danych dzięki czemu EntityState był modyfikowany automatycznie w celu wykonania odpowiedniej operacji CRUD na bazie danych.
W tym wpisie poznamy podejście w tzw. scenariuszu rozłączenia w którym kontekst nie jest świadomy zachodzących operacji – wymagane są dodatkowe kroki związane z ustawieniem odpowiedniego stanu EntityState.
Spójrzcie na poniższy diagram przedstawiający wykonanie operacji dodawania/aktualizacji/kasowania na bazie danych:
Jak doskonale widzicie zmiany dokonywane na poszczególnych encjach nie są śledzone przez DbContext - EntityState nie jest automatycznie modyfikowany. Przeprowadzając takie operacje musimy dokonać dołączenia encji do DbContext wraz z odpowiednim stanem nowej encji. Dopiero ten krok będzie skutkował wykonaniem polecenia INSERT, UPDATE lub DELETE po wywołaniu metody SaveChanges().
Powyższe kroki są jasne. Sprawdźmy jak takie operacje wyglądają w praktyce:
// Odłączona encja
var employee = new Employee() { FullName = "Paweł" };
using(var context = new ApplicationDbContext())
{
// Dołączenie encji do kontekstu
// EntityState: Add
context.Add<Employee>(employee);
// Podejście alternatywne
context.Employee.Add(employee);
context.Entry<Employee>(employee).State = EntityState.Added;
context.Attach<Employee>(employee);
// Zapisanie zmian
context.SaveChanges();
}
W powyższym przykładzie tworzenie nowego użytkownika odbywa się poza kontekstem. Metoda Add pozwala na dołącznie encji do kontekstu oraz zmianę stanu na Added. Metoda SaveChanges() spowoduje wywołanie instrukcji INSERT na określonej bazie danych:
exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employee] ([FullName])
VALUES (@p0);
SELECT [Id]
FROM [Employee]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
',N'@p0 nvarchar(4000)',@p0=N'Paweł'
Dostępne metody
Zanim przejdziemy do omówienia różnego typu operacji zapoznamy się z metodami dostępnymi w obrębie DbContext (DbSet udostępnia ten sam zestaw metod), które pozwalają na dołączenie encji do kontekstu oraz wykonywanie operacji INSERT, UPDATE oraz DELETE. Spójrzmy na poniższą tabelę:
Typ operacji
|
Metoda DbContext
|
Opis
|
INSERT
|
DbContext.Attach
|
Dołączenie encji do DbContext. Stan Unchanged dla encji, której klucz posiada wartość lub stan Added dla encji o pustej lub domyślnej wartości klucza.
|
DbContext.Add
|
Dołączenie encji do DbContext ze stanem Added.
|
DbContext.AddRange
|
Dołącznie kolekcji encji do DbContext ze stanem Added.
|
DbContext.Entry
|
Dostęp do informacji o śledzaniu zmian i operacji dla wskazanej encji. (Brak odpowiednika dla DbSet)
|
DbContext.AddAsync
|
Asynchroniczna metoda dołączająca encję do DbContext ze stanem Added oraz rozpoczęcie śledzenia zmian (dla scenariusza rozłączenia). Dane zostaną wstawione do bazy danych po użyciu metody SaveChangesAsync().
|
DbContext.AddRangeAsync
|
Asynchroniczna metoda dołączająca kolekcję encji do DbContext ze stanem Added oraz rozpoczęcie śledzenia zmian (dla scenariusza rozłączenia). Dane zostaną wstawione do bazy danych po użyciu metody SaveChangesAsync().
|
UPDATE
|
DbContext.Update
|
Dołączenie encji do DbContext ze stanem Modified.
|
DbContext.UpdateRange
|
Dołączenie kolekcji encji do DbContext ze stanem Modified.
|
DELETE
|
DbContext.Remove
|
Dołączenie wskazanej encji do DbContext ze stanem Deleted oraz rozpoczęcie śledzenia zmian.
|
DbContext.RemoveRange
|
Dołączenie kolekcji lub listy encji do DbContext ze stanem Deleted oraz rozpoczęcie śledzenia zmian.
|
Podsumowanie
Scenariusz rozłączenia jest niezwykle powszechny w aplikacjach webowych gdzie kontekst dla każdego żądania jest nowy a obiekty przekazywane są z zewnętrznego źródła. W takiej sytuacji sami musimy ustawić stan encji tak, aby kontekst wiedział czy ma dokonać aktualizacji, dodania czy usunięcia danych.
Prostym przykładem dla scenariusza połączonego (operacji wykonywanych w ramach kontekstu) może być pobranie danych wszystkich pracowników z bazy danych a następnie aktualizacjach tych danych (z jakiegokolwiek powodu):
// Pobranie listy wszytkich pracowników
public void ChangeEmployeeName()
{
try
{
// Wszelkie zmiany dokonywane w obrębie kontekstu
using (var context = new ApplicationDbContext())
{
var employeeList = context.Employee.ToList();
foreach (var emp in employeeList)
{
emp.FullName = $"(Updated: {emp.FullName})";
}
context.SaveChanges();
}
}
catch (Exception ex)
{
// Add Exception Logging
}
}
Przeciwieństem będzie scenariusz rozłączony w którym dane dotyczące pracownika będą przekazane do metody z warstwy prezentacji naszej aplikacji:
// Dane pracownika do aktualizacji przekazane z warstwy prezentacji
public bool UpdateEmployeeName(Employee emp)
{
try
{
using (var context = new ApplicationDbContext())
{
// Rozpoczęcie śledzenie zmian z domyślnym stanem 'Unchanged'
context.Employee.Attach(emp);
// Zmiana stanu na 'Modified'
context.Entry(emp).State = EntityState.Modified;
// Zapisanie zmian -> wykonanie operacji UPDATE na bazie danych
context.SaveChanges();
}
return true;
}
catch (Exception ex)
{
// Add Exception Logging
}
return false;
}
W trzech kolejnych wpisach przejdziemy bardziej szczegółowo przez scenariusz rozłączenia skupiając się na operacjach INSERT, UPDATE oraz DELETE.