W ramach urlopu i siedzenia w domu postanowiłam zaprogramować coś ciekawego w celu poszerzenia swoich umiejętności dotyczących m.in. .NET Core i Web API. Tak powstał pet projekt MyNozbe oparty na Nozbe (aplikacji do zarządzania zadaniami – standardowo udostępniam Wam link afiliacyjny). W trakcie programowania natknęłam się na ciekawy problem, którym chciałam się z Wami podzielić.
Opis struktury danych
Załóżmy, że mamy przygotowany obiekt Project o jakiejś nazwie, który zawiera listę obiektów Task. Każdy obiekt Task ma swoją nazwę Name oraz status IsCompleted. Przykładowy obiekt Project może wyglądać następująco:

Jak widać, jest to projekt o nazwie „TestProject”, który ma przypisane 2 zadania: odpowiednio o nazwach „task1” i „task2”. Zadanie pierwsze nie jest zakończone (IsCompleted = false), natomiast zadanie drugie jest zakończone (IsCompleted = true).
Przykładowy kod pobierający obiekt Project z bazy danych może wyglądać następująco:
await _databaseContext.Projects
.Include(x => x.Tasks)
.FirstOrDefaultAsync(p => p.Id == id)
Opis problemu
Chcielibyśmy podczas pobierania obiektu Project pobrać tylko jego otwarte (IsCompleted = false) zadania (obiekty Task).
Rozwiązanie – Include() + Select() + Where()
Można użyć takiego kodu (użyłam typów anonimowych, żeby nie zaciemniać kodu):
await _databaseContext.Projects
.Select(p => new
{
Id = p.Id,
Name = p.Name,
CreationDateTime = p.CreationDateTime,
Tasks = p.Tasks.Where(t => t.IsCompleted == false).Select(t => new
{
Id = t.Id,
Name = t.Name,
CreationDateTime = t.CreationDateTime,
IsCompleted = t.IsCompleted,
ProjectId = t.ProjectId,
Comments = t.Comments
})
})
.FirstOrDefaultAsync(f => f.Id == id);
Wynik będzie zgodny z oczekiwanym – zostanie nam zwrócony tylko jeden obiekt Task dla projektu, który nie jest zamknięty:

Jednakże to rozwiązanie niezbyt mi się podoba. Pomijając tworzenie typów anonimowych (bo można je przekształcić na mapowanie do konkretnych obiektów modelu), składanie Select() i Where() niezbyt mi pasuje. Kod nie jest zbyt czytelny. Może jest jakieś lepsze rozwiązanie?
Rozwiązanie – IncludeFilter()
Okazuje się, że jest coś takiego jak EF Query Include Filter w paczce nugetowej EFClassic. Pozwala między innymi na Include() połączony z klauzulą Where() – zarówno dla obiektów jeden poziom niżej (jak w moim przypadku) jak i dla bardziej zagnieżdżonych obiektów.
Przykładowy kod operacji wygląda następująco:
await _databaseContext.Projects
.IncludeFilter(p => p.Tasks.Where(t => t.IsCompleted == false ))
.FirstOrDefaultAsync(f => f.Id == id);
Jak widać, nie trzeba tworzyć żadnych nowych obiektów, wystarczy prosty Where(). Kod jest prosty do interpretacji.
Wynik również jest prawidłowy, a także zawiera oryginalną strukturę wszystkich obiektów:

Rozwiązanie – IncludeOptimized()
Odkryłam jeszcze jedną opcję – IncludeOptimized(). Sprawdźmy, jak działa.
Kod wygląda bardzo podobnie, jak w przykładzie z IncludeFilter() – zmieniamy tylko wywoływaną metodę:
await _databaseContext.Projects
.IncludeOptimized(p => p.Tasks.Where(t => t.IsCompleted == false ))
.FirstOrDefaultAsync(f => f.Id == id);
Wynik jest dokładnie taki sam:

Różnice IncludeFilter() i IncludeOptimized()
W czym w takim razie jest różnica? W tym, co się dzieje pod spodem.
Na podstawie opisu w issue można zobaczyć, co się dzieje „pod spodem” IncludeFilter():

Oraz co się dzieje „pod spodem” IncludeOptimized():

Jak widać, wersja druga dzieli query na kilka mniejszych, zamiast jednej dużej. Powinno to skutkować lepszą wydajnością IncludeOptimized() względem IncludeFilter() – ale należy tu pamiętać o założeniu ewentualnego indeksu.
Dodatkowe informacje
Jeśli chcecie dowiedzieć się jeszcze więcej o powyższych rozwiązaniach, zachęcam do zerknięcia do dokumentacji Query IncludeFilter i Query IncludeOptimized.
Podoba Ci się to, co tworzę? Chcesz dostawać informacje o:
– wydarzeniach, które organizuję lub wspieram (np. konferencje, meetupy, webinary)
– inicjatywach, które organizuję lub wspieram (np. GeekWeekWro, DevAdventCalendar)
– moich prelekcjach, kursach i szkoleniach
– wyróżnionych artykułach z mojego bloga
0% SPAMu, 100% informacji! Krótko i na temat.
Chyba najważniejsze, co rzeczywiście idzie do bazy danych, czyli SQL 😉
PolubieniePolubienie
Owszem 🙂 Aczkolwiek jeśli działasz na in-memory db, to nie podejrzysz sql. Można więc to potraktować jako zadanie domowe dla osób zainteresowanych tematem 😉
PolubieniePolubienie
Przy małym projekcie mało ważne, bo i tak wszystko zadziała, a przy wzroście liczby zapytań mogłoby decydować czy trzeba wstawić kolejne pudło do serwerowni, czy nie =)
PolubieniePolubione przez 1 osoba