Przeglądając ostatnio kod który odwoływał się do bazy danych (EntityFramework z LINQ) natrafiłem na kilka rzeczy które przykuło moją uwagę. Musiałem nad nimi trochę posiedzieć gdyż zrozumienie w pełni jak on działa i dlaczego tak – nie należało to do prostych czynności. Problemu by nie było gdyby został zastosowany prosty schemat. Przy wszystkim bardziej skomplikowanym niż wybranie jednego elementu z tabeli czy też pobrania rekordu po ID, należy stosować w bazie widoki.
Wiem, nie jest to zawsze takie proste, bo to jakiś skrypt na bazę trzeba wgrać a my podchodzimy do tego bardziej z kodu, w końcu po to mamy EF itp. Itd. Tak, zgadza się, można mieć różne podejścia i należy wybrać takie którego wszyscy w zespole powinni przestrzegać. Jednak… zanim podejmiecie decyzję o pisaniu tego wszystkiego w kodzie z wykorzystaniem LINQ zastanówcie się nad komplikacjami jakie to niesie za sobą.
Testowanie
Testowanie tego nie należy do prostych rzeczy do napisania. Przeważnie musimy stworzyć albo w pamięci obszerny model bazy danych by nasze JOIN
zadziałały, albo wykorzystać testy integracyjne (nie takie złe). Tak czy siak, mamy pewne ograniczenia i dodatkowy nakład pracy. Bez wykonania jakiejkolwiek z powyższych czynności raczej kodu nam się nie uda przetestować.
Czytelność kodu
To jest bardzo ważne, jak wygląda kod który ma grupowanie i joiny po 5 tabelach w LINQ? Trochę… mało czytelnie. Jeszcze wykorzystując Query Syntax daje radę. Tylko przeważnie to wszystko robimy po to by wybrać… jeden element z listy. Co przy Method Syntax jest czytelne a przy Query Syntax może dodawać zbędne nawiasy, które trochę śmiecą.
Modyfikacje/wsparcie
Jeżeli trzeba dodać warunek, zmienić warunek, usunąć dodać cokolwiek, zawsze wymagany jest pełny deployment. Co przy live i staging ma ręce i nogi, ale przy testowym środowisku czy też naszym developerskim, to zaczyna sprawiać problemy, jeżeli musimy to robić dość często. Odpalić VS, odpalić aplikację, stop, zmienić, odpalić aplikację, stop, zmienić, odpalić… jest to dość czasochłonne i kompletnie nie potrzebne o czym zaraz.
Tutaj warto też wspomnieć o tym, że ciężko się optymalizuje zapytania napisane w LINQ, trzeba się naprawdę postarać i czasami po prostu nie mamy wyjścia a musimy napisać coś niewydajnie.
Zbędne odwoływania do bazy danych
Ok, może i wasza aplikacja szybko działa i uderza co chwila do bazy danych i jest super. Weźmy jednak taki o to przykład:
var data = _ctx.Table.Where(x => x.IsTrue).ToList(); if(data.Count >) { return false; } var workingData = _ctx.Table2.Where(x => data.Contains(x)).ToList();
Pewnie i da się to napisać jakoś rozsądniej, ale teraz zamiast WHERE
wstawcie do każdego zapytania 4 lub 5 JOIN
, GROUPBY
itp. Śmiało, poczekam :)
Więc jak sami widzicie czasami jest ciężko nie zrobić kilku odwołań do bazy.
Rozwiązanie
Rozwiązanie jest dość proste, i aż dziw bierze, że większość po to nie sięga. Tworzymy tabele, relacje, ale nikt nie wpadnie na stworzenie widoku, który zaoszczędzi nam wszystkich możliwych kłopotów:
- Kod możemy łatwo zoptymalizować na bazie.
- Testownie kodu jest proste gdyż co najwyżej musimy wstawić jeden rekord do widoku. Więc ani się nie napracujemy przy pisaniu testów, ani nie będzie też problemów z testami integracyjnymi (tak jak i poprzednio by nie było)
- Modyfikacje i wsparcie jest tak banalne, że nawet nie trzeba zaciągać do tego ludzi którzy potrafią otwierać VS i w nim pracować. Łatwo tym też jest zarządzać – widok jako jednostka deploymentu, statyczna, traktowana jak binarka.
- Jeden widok także załatwi nam zbędne odwoływanie się do bazy danych – jesteśmy wstanie większość rzeczy zamknąć tak by za pomocą jednego zapytania dostać to co potrzebujemy.
Jak widzicie, widoki dają same plusy, i jedynym minusem jest podjęcie decyzji o tym jak je deployować. To znaczy, trzeba to zrobić i trzeba to robić jak już zaznaczyłem poprzez traktowanie widoku, jako binarkę. Pracujemy nad widokiem jak nad kodem – nie przyrostowo (co się zmieniło) ale jako jeden plik. Ale dlaczego jeden? O tym może kiedy indziej.
Przy następnym projekcie, zamiast pisać LINQ zastanówcie się nad stworzeniem widoków, znacząco ułatwi wam to pracę z kodem jak i zwiększy czytelność kodu.
Ja się jednak skłaniam ku procedurom w bazie, a przez ORM’a ich odpalanie.
Na proste wyciągnięcie kilku rekordów, bez dodatkowego warunkowania oczywiście widoki wystarczą
@Łukasz
Tak, procedury są fajne. jednak wszystko z umiarem :) dla mnie procedura jest od tego by zmienić stan danych. Chociaż widzę miejsca gdzie nawet przy pobieraniu procedura by była wstanie coś wnieść. No ale… :) najpierw widoki, potem procedury ;)
Inwazja ORM-ów spowodowała, że ludzie po studiach często nie są w stanie napisać prostego zapytania w SQL-u, nie mówiąc już o jakimś “kriejt wju…”
@PG
nie powiedziałbym, że ludzie po studiach. Większość która zaczęła się bawić ORM i przy nim pozostał ma takie problemy. Ja osobiście też miałem przerwę w T-SQL – jakieś dobre 3-4 lata. Więc łatwo zapomnieć o tym, że można coś lepiej i łatwiej zrobić ;)
Każda moneta ma dwie strony.
Z jednej strony widoki faktycznie mogą znacząco uprościć kod, więc czemu nie stosować ich tam gdzie jest to możliwe. Niestety czasami przychodzi nam tworzyć oprogramowanie w ramach organizacji gdzie jakakolwiek zmiana struktury bazy (czyt. puszczenie skryptu, który coś mieli bezpośrednio na bazie) wymaga złożenia podania w trzech egzemplarzach, z odciskiem kciuka wykonanym własną krwią o północy, koniecznie w noc przesilenia wiosennego. Potem jeszcze tylko tydzień lub dwa oczekiwania na oficjalną zgodę, albo jej brak i …
Dla mnie przerażający jest fakt, że bardzo często decydując się już na użycie LINQ bezpośrednio w kodzie mamy tendencję do pisania tony wyrażeń jedno pod drugim, zamiast jakoś sensownie pogrupować je w dobrze nazwane funkcje. Piszę “mamy tendencję” bo mi też się to zdarza, przecież tak jest prościej, szybciej i wygodniej. A co z refaktoryzacją, co z czytelnością kodu? Kto by się przejmował takimi pierdołami, przecież jest jeszcze tyle do napisania.
Dobrze ponazywane funkcje i zamiast zastanawiać się “co” jest wykonywane możemy po prostu sprawdzić “jak” to jest robione. Niby jedno proste słowo (co/jak), a jednak robi różnicę podczas czytania kodu, szczególnie tego którego nie pisaliśmy sami.
@Glimor
Ano… korporacja może dać… wiem. Ale co zrobić? jak nie ma opcji w ogóle ni jak i mamy działać z tym co mamy koniec kropka. to przełykamy ślinę i jedziemy z tym koxem aż się w końcu all skończy. Wtedy tak jak piszesz – jest wiele spraw do ogarnięcia w tym organizacja kodu itp. Znów jest to maksymalnie skrajny przypadek. Przeważnie jak piszemy apkę to mamy wpływ na jej bazę :)
Chciałbym na chwilę pominąć wydajniejsze rozwiązania jak Dapper + procedury/widoki. Często ludzie zachwyca się w ORM-ie jednym zapytaniem. Jeżeli mamy rozbudowaną relację na bazie, bo prostu wynika to z realnych potrzeb, to wykombinowanie jednego zapytania za pomocą LINQ może nie przynieść zamierzonych efektów. Mam tutaj na myśli EntityFramework – odczytywanie wielu encji z wieloma Include() może być bardzo wolne. Dużo lepiej jest rozbić to na mniejsze zapytania (do bazy) i załadować dane do pamięci wykorzystując Load(). EF podczas głównego zapytania dociągnie dane z pamięci, zamiast generowania wielu join-ów.
https://msdn.microsoft.com/en-us/data/hh949853.aspx (8.2.2 Performance concerns with multiple Includes)
Zgodzę, że ORM to nie jest wydajne narzędzie, ale z drugiej strony wyrzeźbienie skomplikowanych zapytań SQL też zajmuje sporo czasu. Nie mam tutaj na myśli trywialnych przykładów, jak w tym temacie (bez urazy). Przykładowo: hierarchia z wykorzystaniem TPH (table per hierarchy) dla kilku encji + wiele relacji. W LINQ, po krótkiej konfiguracji, to po prostu działa. Zapytania LINQ są dosyć proste i szybko można osiągnąć zamierzony rezultat. Drobna zmiana wymagań/koncepcji = drobna zmiana w LINQ i znowu działa… Często jest tak, że trzeba zdążyć na czas z oddaniem aplikacji, a dopiero później przychodzi moment na optymalizację.
Stosuję różne podejścia, w zależności od potrzeb oraz dostępnego czasu. Nie bronię tutaj ORM-a, ale ręcznie pisanie zapytań też ma swoje wady. Czasami w ogóle nie ma możliwości skorzystania z ORM-a lub widoków – np. dynamiczne odpytywanie bazy, gdy struktura jest znana dopiero podczas runtime-u.
@Mad
Kilka rzeczy, bo ja tutaj o wydajności zapytania w ogóle nie pisałem i tego tematu nie poruszałem i nie zamierzam poruszać. Każdy będzie miał swoją własną religię tutaj. Więc wchodzenie w dyskuję czy Dapper, czy Simple.Data, czy pure SQL (najwydajniejszy) nie ma sensu. Do tego, nie mówię tutaj o tym kto i jak konwertuje co i jak na SQL. Nie, to mnie nie interesuje na tyle by to poruszać. Każdy ORM ma swoje wady i zalety.
Co do przykładów, dałem maksymalnie błahy, po takie są najczęstsze. Jakbym tutaj dał normalny, z życia wzięty przykład to raczej bym odstraszył albo poruszył temat: “kto tą bazę projektował”. A nie o to chodziło. Przykład więc idealnie dobrany został. Co do tego, że pewne rzeczy łatwiej jest za pomocą modelu i konfiguracji w ORM uzyskać to mam wrażenie, że piszesz o zwracaniu bardzo skomplikowanego data setu z relacjami (zaciągniętymi) z bazy danych na raz. Co już samo w sobie jest… fatalnym pomysłem. I na szczęście jeszcze w życiu nie natrafiłem na przypadek by to było potrzebne. Ale też nigdy nie pisałem aplikacji typu Excel ani nie potrzebowałem całej bazy by zrobić obliczenia czegoś. Zasada jest prosta, View to z denormalizowany widok na bazę danych. Koniec kropka, nie ma żadnych relacji, nic nie muszę zaciągać dodatkowo. Jeżeli zaś user będzie chciał wejść w szczegóły to się dociąga to co chce on zobaczyć. A nie wszystko bo może user będzie chciał coś to zobaczyć?
Co do pisania optymalnego View a LINQ – nikt nie mówił, że z View będzie banalnie prosto. Ale nie będzie na pewno trudniej niż z LINQ – który sam w sobie ma ograniczenia.
Nawiązując do czasu… sory to jest argument, którego nie akceptuje, kategorycznie. Co to znaczy nie ma czasu? masz czas spędzić 20-30 min wymyślając LINQ? to masz czas na to by napisać to w T-SQL.
Mam wrażenie, że cały komentarz jest o wydajności i o tym co jest prościej napisać a co trudniej. Czyli ogólnie trochę nie na temat posta :) a więc poległem i nie poprawnie, nie wyraźnie określiłem to co chciałem przekazać, sorki :)
@Gutek
“Co już samo w sobie jest… fatalnym pomysłem. I na szczęście jeszcze w życiu nie natrafiłem na przypadek by to było potrzebne.” – np. rozbudowane raporty.
LINQ jest dla mnie bardzo naturalne (w formie fluent API), w przeciwieństwie do SQL, przez co zajmuje mi znacznie mniej czasu. Zaletą ręcznego SQL jest wydajność. Jeśli tego nie ma, nie widzę za bardzo innych zalet. W C# jest mi wygodniej, szybciej, łatwiej w refactoringu itd. Minusem są dla mnie niuanse ORM-a lub migracje do nowej wersji, bo MS lubi ubijać swoje projekty co jakiś czas (btw, ten nowy EF core to porażka, ma pełno bugów a wydali jako RC).
Od dłuższego czasu, ciężko mi znaleźć ciekawe posty. Większość osób opisuje pewne podejścia, technologie podając jako przykłady banalne rzeczy. Można mieć wrażenie, że wszyscy pracują nad rozwijaniem prostego CRUD-a… Pamiętam Twoje dawne posty ze starego bloga, często były to trudniejsze ciekawostki – niektóre “hardcorowe”, ale fajnie się czytało. Brakuje właśnie realnych przykładów, podzielenia się doświadczeniem, co się sprawdziło i w jakich przypadkach (w większych projektach). Zapominasz chyba o bardziej doświadczonych czytelnikach :)
@Mad
:) no widzisz, właśnie z parunastu hardcorowych LINQ query po 10-15 joinów, group by, maksymalnie nieczytelnego kodu tworzą się takie proste praktyki. Tylko wrzucanie kodu nic nikomu nie powie, bez opisania całej specyficznej sytuacji projektowej. Do tego zostałem poproszony by nie używać kodu firmowego w żadnej postaci.
Bo do póki ty piszesz ten kod, to jest super. Ale osoba która po Tobie przejmie aplikację, nie koniecznie będzie wiedziała co miałeś na myśli i dlaczego tak.
Dodając do tego samą czytelność kodu… przy skomplikowanym LINQ sorki, to nigdy nie jest czytelne, nie jesteś wstanie zrobić by było (nie licząc extension methods które może coś pomogą). Przy View zaś prościej się nie da. I jak potrafisz LINQ to T-SQL napiszesz prawie tak samo szybko.
Co do hardcorowych dev artów. Żeby napisać coś takiego musi człowiekowi zależeć. A mi coraz rzadziej na pewnych technologiach zależy a z nimi mam najwięcej styczności :) liczę na to, że elixir to zmieni. Zobaczymy :)
Ps.: raporty to excel ;)
[…] podejście jest złe, bo wymaga więcej pisania kodu. Cały czas powtarzamy jakieś kod, w ogóle nie wykorzystujemy mocy LINQ pomijając JOINy, zero re-używalności […]
Nie ferowałbym tutaj wyroków tak łatwo. Oczywiście, w wiekszosci masz racje, jednak pamietajmy, że w większości przypadków widok to nic innego jak nazwany SELECT. Ostatnio naciąłem się na to przy MySQL. Zacząłem ambitnie od widoków ale okazało się, że tak skomplikowane zapytania (jakie musiałem pisać) wykluczały użycie widoków, bo było to maksymalnie niewydajne!
i w ORMie napisałeś lepsze czy może wykorzystałeś inne opcje dostępne w MySQL?
Comments are closed.