W tym wpisie zamieszczę kilka wskazówek i informacji jak w miarę bezboleśnie rozpocząć naukę i pracę z Linq i na co warto zwrócić uwagę. Praca z linq daje możliwość obsługi wielu źródeł danych za pomocą tych samych metod, jako przykład wykorzystam bazę danych, którą pokazałem kilka wpisów temu. Do połączenia z bazą danych używam EntityFramework. Dla przypomnienia schemat poniżej.
Składania
W linq mamy możliwość zapisania wyrażenia na dwa sposoby. Pierwszym z nich jest tzw. query expression wyrażenie zapisane w ten sposób przypomina, swoją budową zapytanie języka SQL.
1 2 3 |
var clients = from c in Clients where c.Name.StartsWith("R") select c; |
Drugim sposobem, zapisania tego same wyrażenia jest tzw. method-based query. Zapytania piszę tutaj za pomocą metod rozszerzających oraz wyrażeń lambda.
1 2 |
var clients =context.Clients .Where(client => client.Name.StartsWith("R")); |
Jak widać, nie korzystam tutaj z metody Select, w większości przypadków jest ona opcjonalna. Czy któraś z tych składni jest lepsza? Moim zdaniem nie. To od programisty zależy, która składnia bardziej przypadnie mu do gustu. Osobiście, zaczynałem naukę linq od pisania w składni, która przypominała SQL, ale szybko przesiadłem się na wyrażenia lambda.
Powyższe dwa sposoby pisania zapytań można ze sobą łączyć.
1 2 3 4 |
var clients = from c in Clients where c.Name.StartsWith("R") select c; clients = clients.Where(c=>c.Name.Length>3); |
Do nauki, pisania wyrażeń linq polecam LinqPad. Narzędzie to potrafi zamienić wyrażenie napisane w jednej składni na drugą, potrafi również pokazać jakiego typu są wyniku takiego wyrażenia oraz jakie zapytanie Sql zostanie wykonanie na bazie danych.
Lazy loading
Lazy loading to mechanizm, który sprawia, że wyrażenie nie zostanie wykonane dopóki, dane nie będą nam potrzebne. Co to oznacza? Weźmy pod lupę poprzedni przykład. Zapisaliśmy wyrażenie, które zwróci wszystkich klientów, których imię zaczyna się na R. W następnej linijce dodaliśmy kolejny warunek, który sprawdza czy imię klienta jest dłuższe niż 3 znaki. Jakie zapytanie zostanie wygenerowane? Czy najpierw zostaną zwrócone wszyscy klienci, których imię zaczyna się od R, a następnie zostaną przefiltrowani, w pamięci komputera? Czy może te dwa warunki zostaną złączone w jeden i tak przekazane do bazy danych? Sprawdźmy.
Jak widać, warunki zostały połączone i zapytanie zostało całkowicie wykonanie na bazie danych. Czyli żadne filtrowanie nie odbywa się pamięci. Kiedy jednak wykonywane jest to zapytanie? Tak, jak pisałem wtedy gdy dane są nam potrzebne, czyli w momencie rozpoczęcia iterowania po kolekcji clients. Zmienimy teraz nasze, zapytanie.
Wyrażenie, daje taki sam efekt, jednak zapytanie jest zawiera tylko jeden warunek co oznacza, że drugi warunek sprawdzany jest gdy dane są w już w pamięci. Dlaczego? Ponieważ, skorzystaliśmy tutaj z metody, toList. Metoda ta jest zachłanna i wszystkie dane, ładuje do pamięci naszego komputera przez co zmienia się nasze zapytanie. Ok, więc lazy loading to dobra rzecz trzeba być tylko świadomym, tego kiedy wykonuję się nasze zapytanie i zbudować nasze wyrażenie tak, aby jak najwięcej działo się po stronie bazy danych.
Rozważmy jednak taki przypadek. Pobierzmy, wszystkich klientów i spróbujmy wyświetlić liczbę zamówień dla każdego z nich.
Jak widać, nie wykonujemy tutaj tylko jednego zapytania. Pierwsze zapytanie pobiera, wszystkich klientów a następnie dla każdego z nich pobierana jest liczba zamówień osobnym zapytaniem. Mamy więc w sumie N+1 zapytań, gdzie N to liczba klientów w bazie danych. Nie za fajnie, ponieważ każde zapytanie to zmarnowany czas. Nie dało by się tego zrobić jednym zapytaniem z jednym prostym joinem? Jasne, że by się dało.
Eager loading
Eager loading jest to mechanizm, dzięki któremu możemy określić jakie dane powiązane z danym obiektem chcemy pobrać. Na przykład, w naszym przypadku chcielibyśmy, aby z klientami pobrane zostały również zamówienia.
Jak, widać korzystając z metody Include, wygenerowaliśmy jedno zapytanie, które zwraca nam wszystkie potrzebne dane. Jednak jak widać zapytanie zwraca, o wiele więcej danych niż jest nam faktycznie potrzebne. Chcemy znać, tylko ilość zamówień a dostajemy wszystkie kolumny tabeli zamówienia. Należy mieć to na uwadze i wybrać pomiędzy lazy loading a eager loading. Powyższy sposób rozwiązania tego problemu nie jest cechą linq tylko EntityFramework, ale stwierdziłem, że warto tutaj to zamieścić ponieważ często te dwie technologie wykorzystuje się razem. Po za tym podobnie można rozwiązać, ten problem korzystając z innych bibliotek ORM np. NHibernate.
Wracając jednak do linq, czy nie da się napisać tego zapytania korzystając tylko z linq? Da się np. w ten sposób.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var ordersByClients = context.Clients .GroupJoin(context.Orders, c => c.Id, o => o.Client_Id, (c, co) => new { c, co }) .SelectMany(temp0 => temp0.co.DefaultIfEmpty(), (temp0, o) => new { ClientName = temp0.c.Name, NumberOfOrders = temp0.co.Count(order => (order.Client_Id == temp0.c.Id)) }) .Distinct(); foreach (var data in ordersByClients) { Console.WriteLine("Klient {0} ma {1} ", data.ClientName, data.NumberOfOrders); } |
lub w ten sposób
1 2 3 4 5 6 7 8 9 10 11 |
var ordersByClients = (from c in context.Clients join o in context.Orders on c.Id equals o.Client_Id into co from o in co.DefaultIfEmpty() select new { ClientName = c.Name, NumberOfOrders = co.Count(order => order.Client_Id == c.Id) }).Distinct(); foreach (var data in ordersByClients) { Console.WriteLine("Klient {0} ma {1} ", data.ClientName, data.NumberOfOrders); } |
Przyznacie jednak, że te zapytania nie wyglądają zachęcająco i moim zdaniem, lepiej skorzystać np. z EntityFramework, który takie zapytania pisze za nas.
Najnowsze komentarze