Bazy danych · C# · Programowanie

EntityFramework – IncludeFilter() i IncludeOptimized()

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.

3 myśli na temat “EntityFramework – IncludeFilter() i IncludeOptimized()

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s