Wprowadzenie
Pracując z Entity Framework możemy doceniać jego moc oraz zapomnieć o tym, jaką potężną magią on operuje. Np. skąd ten diabeł wie, co się zmieniło, a kiedy muszę mu o tym przypomnieć.
Mam prostą regułę w głowie, jeśli wyciągam dane z kontekstu i zaraz od razu wrzucam je tam z powrotem, nie ustawiam do tego flag, nie wtrącam się, nie robię hokus-pokus i zakładam, że wszystko będzie dobrze.
Jeśli wyciągam dane na dłużej, wtedy podpowiadam EF, żeby nie śledził zmian, bo to trochę zajmie, zanim te dane wrócą do bazy danych i szkoda zachodu.
Dane można zapisać / zaktualizować na kilka sposobów:
Na raz:
- Pobierz
- Zmodyfikuj
- Zapisz
Na dwa
- Utwórz
- Przypnij
- Zapisz
Na trzy:
- Nie zaglądam, jeśli działa u Ciebie to dawaj dawaj na produkcje
Wygląda że proste? To sprawdzam, co i jak zadziała
W obu przypadkach mam taki oto kod:
public class BlogDbContext : DbContext | |
{ | |
public BlogDbContext(DbContextOptions<BlogDbContext> options) : base(options){} | |
public DbSet<Post> Posts { get; set; } | |
} | |
public class Post | |
{ | |
public Guid Id { get; set; } | |
public string Title { get; set; } | |
public DateTime ReleaseDate { get; set; } | |
public DateTime? UpdateDate { get; set; } | |
} |
Nie oszukujmy się, bunkrów nie ma, kontekst oraz jakiś POCO na potrzeby przykładów.
Uno
var m = _blogDbContext.Posts.Single(x => x.Id == id); | |
m.UpdateDate = DateTime.Now; | |
m.Title = title; | |
_blogDbContext.SaveChanges(); | |
Ten przykład działa, pobieram, modyfikuje, zapisuje. EF robi magię i potrafi sam wykryć zmianę w encji – nie wymaga wywołania `update` aby znaleźć i nanieść zmiany na obiekcie. PFM! Wymagany jest natomiast strzał do bazy aby pobrać początkowy obiekt do modyfikacji.
Na plus – nie tracę informacji, który nie modyfikuje. Wrócę do tego niżej.
Duo(s)
Teraz się zacznie, co się stanie, gdy wyciągnę dane bez śledzenia i zapiszę zamiany:
var m = _blogDbContext.Posts.AsNoTracking().Single(x => x.Id == id); | |
m.UpdateDate = DateTime.Now; | |
m.Title = title; | |
_blogDbContext.SaveChanges(); |
Pudło! Nic, EF cichutko “zapisał” zmienione encje, ale mój odcięty obiekt nie był na tej liście, w związku z tym – nic się nie wydarzyło. Może tylko odrobina smutku. Ale sam tego chciałem, przecież mówiłem – bez śledzenia!
Spróbuje inaczej, stworze nowy obiekt, przypnę go do kontekstu i wtedy zapisze:
var m = new Post | |
{ | |
Id = id, | |
Title = title, | |
UpdateDate = DateTime.Now | |
}; | |
_blogDbContext.Posts.Attach(m); | |
_blogDbContext.SaveChanges(); |
Hus! (Dzięki panie Łukaszu) – nie działa! Ale czemu? Przecież obiekt jest w kontekście!
/// <summary> | |
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | |
/// the same compatibility standards as public APIs. It may be changed or removed without notice in | |
/// any release. You should only use it directly in your code with extreme caution and knowing that | |
/// doing so can result in application failures when updating to a new Entity Framework Core release. | |
/// </summary> | |
public override EntityEntry<TEntity> Attach(TEntity entity) | |
{ | |
var entry = EntryWithoutDetectChanges(entity); | |
SetEntityState(entry.GetInfrastructure(), EntityState.Unchanged); | |
return entry; | |
} |
Ach, obiekt został dodany, ale jest oznaczony jako unchanged! Ok-sprawdzam:
var m = new Post | |
{ | |
Id = id, | |
Title = title, | |
UpdateDate = DateTime.Now | |
}; | |
_blogDbContext.Posts.Attach(m); | |
m.Id = id; | |
_blogDbContext.SaveChanges(); | |
return RedirectToAction("Index"); |
Czyli co, nawet jeśli mam nowy obiekt i przypiąłem go do kontekstu nadal nie jest śledzony? Nie koniecznie, obiekt został przypięty i jest śledzony, ale zmiany nie nastąpiły po włączeniu śledzenia. Natomiast taki kod:
var m = new Post | |
{ | |
Id = id, | |
}; | |
_blogDbContext.Posts.Attach(m); | |
m.Title = title; | |
m.UpdateDate = DateTime.Now; | |
_blogDbContext.SaveChanges(); |
Robi już robotę, zwróć uwagę, że zmiany następują po przypięci i włączeniu śledzenia zmian
Zamiast takiego cyrkolenia się, można iść na skróty i wywołać po prostu Update:
var m = new Post | |
{ | |
Id = id, | |
Title = title, | |
UpdateDate = DateTime.Now | |
}; | |
_blogDbContext.Update(m); | |
_blogDbContext.SaveChanges(); |
Który ustawia status obiektu na modified i zapisuje zmiany.
Turbo mega uwaga:
Oba przypadku zerują wartość dla ReleaseDate który dostanie domyślną 01.01.0001 00:00:00 wartość-trzeba tego pilnować. W przypadku gdy wcześniej ładujemy obiekt z DB a potem go modyfikujemy, ReleaseDate jest ustawiony i nie nadpisany zerową wartością.
Pewnie można by poszukać i podpowiedzieć, które właściwości zostały zmienione, ale to wymagałoby znajomości voodoo.
Dodam tylko dla optymalizacji, że gdy wyciągamy obiekt z EF, który nie będzie zmieniany, np. tylko żeby wyciągnąć i wyświetlić, warto użyć opcji `
AsNoTracking, aby podpowiedzieć EF zaprzestania śledzenia obiektów, bo zniknąć one z naszego kontekstu.
var movies = _blogDbContext.Posts.AsNoTracking().ToList(); | |
return View(movies); |
Jeśli zaistnieje potrzeba ponownego podpięcia, można zrobić attach lub update – ale to już wiesz.
Szczegóły tutej:
https://learn.microsoft.com/en-us/ef/core/change-tracking/
ps.
Tak naprawdę to nie korzystam z EF.
ps2.
Źródło całego pliku cs dostępne tutaj:
https://gist.github.com/jstadnicki/9a97619c26e4ba9571fa2aec40be25d3