Korzystając z Entity Frameworka warto zwracać uwagę na sposób pobierania danych.
W przypadku, gdy nie będziemy ich modyfikować, a chcemy jedynie pobrać dane tylko do odczytu, przydatna okaże się metoda AsNoTracking(). Wywołanie metody skutkuje brakiem śledzenia danych przez kontekst. Dzięki temu nie marnujemy niepotrzebnie zasobów.
Przyjrzyjmy się poniższemu przykładowi, w którym porównamy pobieranie danych z i bez metody AsNoTracking(). Na początek pobieramy dane standardowo. W tym celu modyfikujemy metodę Index() kontrolera LocationsController i debugując przechodzimy za linię kodu odpowiadającą za zwrócenie danych do widoku. W okienku watch podglądamy nasz obiekt kontekstu i widzimy, że 4 lokalizacje zostały do niego zapisane.
Teraz czas na wywołanie metody AsNoTracking() i upewnienie się, że dane nie są zapisywane w kontekście. Tym razem metoda Index() prezentuje się następująco:
1 2 3 4 5 6 7 8 9 |
public ActionResult Index() { var locations = _db.Locations.AsNoTracking(); return View(locations.ToList()); } |
Debugujemy, przechodzimy za ostatnią linię kodu w metodzie Index() i podglądamy nasz obiekt kontekstu.
Jak widać lokalizacje nie zostały tym razem zapisane do kontekstu, nie są przez niego śledzone. W tym przypadku oszczędności pamięci są niewielkie, ale wyobraźmy sobie gdybyśmy mieli takich lokalizacji tysiące, spowodowałoby to niepotrzebne straty. Dlatego należy zwrócić szczególną uwagę w takich sytuacjach i stosować AsNoTracking(), tam gdzie tylko jest to możliwe.
Dobre info. Z .AsNoTracking() już co prawda się spotkałem przy pracy z 1,5 miliona rekordów. Można było odczuć, że EF mocno obciąża taka zabawa. .AsNoTracking() wtedy pomogło.
Z kolei polecałbym pracę w taki sposób, że nie pobieramy obiektów z bazy bezpośrednio, ale w formie konkretnych ViewModeli, na przykład zamiast:
return View(context.Users.ToList());
robimy:
return View(context.Users.Select(e => new UserViewModel { Login = e.Login, Email = e.mail }));
Zazwyczaj ma to sens, bo w warstwie widoku nie wszystkie property są nam potrzebne i najczęściej nie będziemy tego modyfikować tylko wyświetlać.
Ponadto benefity są trzy:
1. ViewModele nie są trackowane oczywiście, bo to inne obiekty, nie z contextu, więc ich koszt jest niski.
2. Zapytania do db są krótsze, ponieważ pobrane zostaną tylko wymagane pola, a nie każde property z obiektu.
3. Oszczędzimy czas na materializacji obiektów.
Dzięki za feedback! Będę miał to na uwadze 🙂
A. Płoński – do Twoich trzech punktów dodałbym, że viewmodele pozwalają nam na zachowanie zasady SRP i osiągnięcie lepszej architektury, dzięki czemu później możemy łatwiej rozbudowywać aplikację.
Swoją drogą – po co pobierać aż1,5 mln rekordów na raz? To chyba był główny problem, nie brak AsNoTracking().
@dwdkls – W zasadzie najlepiej jest stosować obiekty DTO i do nich wypluwać dane z EF. DTO od ViewModelu różni się i to znacznie. Sporo osób myli te dwie koncepcje. Obiekt DTO z definicji zawierać powinien jedynie nasze property i nie może zawierać żadnej logiki, czyli metod albo jakiejś innej obróbki. Z kolei ViewModel może już zawierać metody i jakąś tam logikę potrzebną do warstwy prezentacji.
Często do widoku Razora w MVC przekazuję ViewModel, który zawiera w sobie wiele DTO jako property lub listę DTO. Na przykład klasa:
public ClientsViewModel
{
public OwnerDTO ClientsOwner { get; set; }
public List Clients { get; set; }
}
Następnie w widoku nasz ClientsViewModel pozwala nam pokazać:
Klienci pana @Model.Owner.Name
@foreach(ClientDTO client in Model.Clients)
{
@client.Name
}
Dzięki temu przekazaliśmy do metody tylko jeden ViewModel, który zawiera wszystkie obiekty jakie chcemy pokazywać w widoku. Jest to lepsze rozwiązanie niż pisanie cudów typu:
ViewModel.Owner = owner; // typu OwnetDTO
return View(clients); // typu List
no i rzutowanie z ViewBaga na obiekty znów, itd… Pomyślcie o cache takiego ViewModelu. Przecież nie będziecie cachować ViewBaga i listy.
Daje nam to dodatkowo wielką zaletę oraz rozwiązuje problem z jakim na pewno w końcu się spotkamy. Wyobraźcie sobie, że nagle każą wam zrobić serwis, który będzie zasilał danymi aplikację mobilną budowaną przez kogoś zupełnie innego. Ten ktoś wymaga od nas listy klientów, ale bez tego naszego ownera, którego pokazujemy w wersji MVC.
Robimy na przykład w WebAPI metodę GET /api/Clients/{id} i zwyczajnie zwracamy zserializowaną do JSONa listę List.
Wszystko dzięki temu, że mamy jasno podzielone obiekty do transportu (DTO – bez logiki) oraz wyświetlania (ViewModele).
Dobrą praktyką jest aby warstwa waszego kontrolera a tym bardziej widoku nie miała zareferowanego namespace’u z modelem na którym operuje EF. Kontroler i widok ma znać tylko DTO. Dane pobierać może z jakiejś klasy / namespace’u Repo lub DAL, który to gada z EF, ale i nie tylko, bo zamiast dzwonić po dane do EF przy każdym wywołaniu może serwować je z cache albo innych źródeł danych poza EF. A najlepsze, że kontroler/widok i tak zawsze otrzyma obiekty DTO nie wiedząc skąd one pochodzą.
W ogóle tutoriale uczące jak korzystać z EF i budować IQueryable w kontrolerach są sporym uproszczeniem i mało mają wspólnego z architekturą prawdziwej aplikacji.
Co do drugiej części:
1 500 000 obiektów w zwyczajnych aplikacjach faktycznie może się nie pobiera, a przecież nawet limitujemy po 50 rekordów na stronę. Wszystko jednak zależy, bo mam i taką aplikację, gdzie pobieram 3 000 000 obiektów EF do pamięci (AsNoTracking oczywiście) i z nich generuję raporty, w których przedstawiam te dane na różne sposoby: filtruję po kategoriach, sortuję, sumuję wartości, agreguję, itd… Wychodzi taniej niż robić 75 zapytań operujących, filtrujących, sumujących i sortujących te 3 000 000 rekordów w bazie. Taniej wczytać je do pamięci i na tym porobić listy, które nas interesują.
Dzięki za przypomnienie. Człowiek pamięta a potem nie używa.