EF Plus – aktualizacja wielu obiektów jednym zapytaniem
Bardzo lubię wykorzystywać Entity Framework w swoich projektach. W szczególności lubię je za łatwość i szybkość tworzenia kodu (na przykład za opisywany ostatnio mechanizm migracji). Ale żeby nie było tak różowo, Entity Framework ma również swoje problemy. Są one głównie związane z wydajnością. Szczególnie, że niektóre operacje, który wykonalibyśmy w czystym SQLu jednym prostym zapytaniem, w Entity Framework wymagają wielu operacji na bazie danych.
Daniel Plawgo. .NET Developer w SoftwareHut, Trainer, Consultant, Bloger (plawgo.pl). Od wielu lat jest związany ze społecznością Microsoft. Początkowo był Student Partnerem i prowadził grupę .NET Eastgroup na Wydziałe Matematyki i Informatyki UWM. Później był Student Consultantem odpowiedzialnym za studenckie grupy .NET z północnej Polski. Od prawie 10 lat prowadzę zawodową grupę .NET OLMUG u nas w Olsztynie.
Spis treści
Problem?
Wyobraźmy sobie, że potrzebujemy dodać do aplikacji funkcjonalność usuwania kategorii produktów. Mając na myśli usuwanie, nie chodzi mi o fizyczne usunięcie danych z bazy, tylko oznaczenie, że są usunięte (każdy rekord w bazie ma kolumnę IsActive). W przypadku usuwania kategorii chcemy również usunąć wszystkie produkty przypisane do danej kategorii.
W testowej aplikacji klasy dla kategorii oraz produktu wyglądają tak:
public class BaseModel { public BaseModel() { IsActive = true; } public int Id { get; set; } public bool IsActive { get; set; } }
public class Category : BaseModel { public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } }
public class Product : BaseModel { public string Name { get; set; } public int CategoryId { get; set; } public virtual Category Category { get; set; } }
BaseModel jest klasą bazową dla wszystkich modeli. W przykładzie w niej znajduje się właściwość Id (klucz główny w tabeli) oraz właściwość IsActive określająca, czy obiekt jest usunięty, czy nie.
Usunięcie kategorii oraz powiązanych produktów w czystym Entity Framework może wyglądać mniej więcej tak:
private static void SoftDelete() { using (var db = new DataContext()) { var category = db.Categories.FirstOrDefault(c => c.IsActive); if (category == null) { return; } category.IsActive = false; foreach (var product in category.Products) { product.IsActive = false; } db.SaveChanges(); } }
W pierwszej kolejności pobieram kategorie z bazy (pierwszą aktywną kategorię, dzięki czemu przykład można uruchamiać wielokrotnie). W przykładzie w metodzie Seed uzupełniane są dane testowe za pomocą biblioteki Bogus (10 kategorii oraz 100 produktów).
Następnie sprawdzam, czy kategoria została pobrana. Gdy została, ustawiam jej właściwość IsActive na false, a następnie przechodzę po wszystkich powiązanych produktach i dla nich również ustawiam IsActive na false.
Na końcu wywołuję metodę SaveChanges, które aktualizuje dane w bazie.
Powyższy kod można by jeszcze nieco usprawnić, np. dodając Include podczas pobierania kategorii, aby w jednym zapytaniu pobrać również produkty. Ale tym momencie nie jest to główny problem, który chcemy rozwiązać.
Wynik powyższego działania w profilerze wygląda tak:
Jak widać, w pierwszej kolejności wykonały się dwa selecty, które pobrały kategorie oraz produkty. Później w transakcji są wykonywane update’y. Jeden dla kategorii, a pozostałe dla produktów – jedno zapytanie dla każdego produktu.
Tutaj właśnie widać jeden z problemów z Entity Framework. W przypadku takiej bardzo prostej aktualizacji danych Entity Framework generuje wiele niepotrzebnych zapytań. Gdybym w bazie miał przypiętych 100 tysięcy produktów do kategorii, wtedy wykonało by się 100 tysięcy update’ów, mimo że można zrobić to samo jednym zapytaniem.
Entity Framework Plus
Z pomocą w rozwiązaniu tego problemu przychodzi nam darmowa biblioteka Entity Framework Plus. Udostępnia ona między innymi funkcjonalność Batch Update, którą wykorzystam do rozwiązania naszego problemu.
Batch Update umożliwia wygenerowanie update’u w nieco inny sposób. Możemy przede wszystkim zapisać warunek, który efektywniej określi rekordy do aktualizacji, niż ma to miejsce w przypadku przekazywania poszczególnych identyfikatorów produktów. Najlepiej zobaczyć działanie Batch Update w kodzie:
private static void SoftDeleteWithEFPlus() { using (var db = new DataContext()) { var category = db.Categories.FirstOrDefault(c => c.IsActive); if (category == null) { return; } category.IsActive = false; db.Products.Where(p => p.CategoryId == category.Id) .Update(p => new Product() { IsActive = false }); db.SaveChanges(); } }
Podobnie jak wcześniej pobieram kategorie oraz ustawiam właściwość IsActive na false. Natomiast kolejny krok jest już zupełnie inny. Tworzę zapytanie Linq z Where, w którym określam, jakie produkty mnie interesują (w określonej kategorii). I na końcu wywołuję nową metodę Update, która została dodana przez Entity Framework Plus. W niej określam, jakie kolumny w bazie mają zostać zaktualizowane oraz na jaką wartość (ustawiamy IsActive na false).
Po uruchomieniu tego kodu w profilerze widać, że liczba zapytań dość mocno się zmniejszyła:
W tym momencie mamy tylko dwie komendy Update. Jedna aktualizuje kategorie, a druga aktualizuje wszystkie produkty w jednym zapytaniu. W dolnej części zrzutu profilera widać, jakie zapytanie zostało wygenerowane przez Entity Framework Plus. Nie jest ono idealne, ale jest dużo lepsze od tego, co jest w zwykłym Entity Framework.
Transakcje?
Analizując wykonane zapytania w profilerze, można zauważyć, że aktualizacja produktów wykonała się poza transakcją utworzoną przez metodę SaveChanges.
Tak faktycznie się dzieje i warto o tym pamiętać. Metoda Update od razu wykonuje zapytanie na bazie, bez czekania na wywołanie SaveChanges. Nawet niewywołanie SaveChanges spowoduje, że dane zostaną zaktualizowane przez metodę Update. Dlatego powyższy kod nie jest do końca poprawny i trzeba go nieco zmodyfikować, aby rozwiązać ten problem. Można dodać ręcznie transakcję, w której wykonają się zmiany z metody Update oraz SaveChanges:
private static void SoftDeleteWithEFPlusWithTransaction() { using (var db = new DataContext()) { using (var transaction = db.Database.BeginTransaction()) { var category = db.Categories.FirstOrDefault(c => c.IsActive); if (category == null) { return; } category.IsActive = false; db.Products.Where(p => p.CategoryId == category.Id) .Update(p => new Product() { IsActive = false }); db.SaveChanges(); transaction.Commit(); } } }
Tym razem oba update’y są wykonywane w transakcji:
Usuwanie
Entity Framework Plus również wspiera Batch Delete. Działanie tego mechanizmu jest bardzo podobne jak w przypadku Batch Update. Różnica polega na tym, że zamiast metody Update wywołujemy metodę Delete:
private static void DeleteWithEFPlus() { using (var db = new DataContext()) { using (var transaction = db.Database.BeginTransaction()) { var category = db.Categories.FirstOrDefault(c => c.IsActive); if (category == null) { return; } category.IsActive = false; db.Products.Where(p => p.CategoryId == category.Id) .Delete(); db.SaveChanges(); transaction.Commit(); } } }
W tym przypadku również na bazie wykonuje się pojedyncze zapytanie:
Fakt, jest już ono trochę bardziej rozbudowane.
Przykład
Na githubie znajduje się przykład do tego wpisu. Po jego pobraniu należy ustawić w app.config poprawnego connection stringa. W klasie Program znajdują się poszczególne metody, które należy wywołać w metodzie Main.
Podsumowanie
Entity Framework jest bardzo fajną biblioteką do pracy z bazami danych. Ale warto pamiętać o niektórych problemach, które występują w pracy z nim. Na szczęście niektóre problemy można rozwiązać, wykorzystując dodatkowe biblioteki, takie jak Entity Framework Plus. Dzięki nim część operacji można wykonać dużo bardziej efektywnie.
Entity Framework Plus poza Batch Update oraz Batch Delete udostępnia też kilka innych ciekawych opcji, które opiszę w jednym z kolejnych wpisów. Dodatkowo warto się jeszcze zainteresować płatną wersją biblioteki o nazwie Entity Framework Extensions.
Artykuł został pierwotnie opublikowany na plawgo.pl. Zdjęcie główne artykułu pochodzi z unsplash.com.