poniedziałek, 20 kwietnia 2009

Lazy Loading a sprawa wydajności




Podążając za ciągiem skojarzeń: od przedwczorajszego DAO przez wczorajsze Lazy Loading dziś czas na aspekt wydajności podczas korzystania z maperów relacyjno-obiektowych. Właściwie to główne niebezpieczeństwo tkwi w tytułowym Lazy Loadingu.

Sprawa jest w sumie prosta - trzeba uważać co się robi i mniej więcej orientować się jak działa mapper. Nie trzeba wnikać nawet w aspekty implementacyjne Lazy Loadingu: klasy pośredników, które mogą być podstawiane do encji czy to poprzez manipulację byte codem czy w czasie generowania kodu źródłowego (jednak zainteresowanym polecam zgłębienie tematu - warto wiedzieć "jak oni to zrobili").

Niestety na żadnej ze stron domowych maperów zgodnych z JPA (Hibernate, OpenJPA, Kodo, TopLink) nie dopatrzymy się ani na głównej stronie, ani na żadnej innej wielkiego czerwonego napisu "UWAGA NA WYDAJNOŚĆ PODCZAS STOSOWANIA LAZY LOADING!". Nie uraczymy również zestawu paru prostych porad, które uchroniły by nas przed dramatycznym spadkiem wydajności. Czyżby nie wyglądało to zbyt marketingowo? Może autorzy sądzą, że ludzie tego nie zauważą i jakoś to będzie - kolejny bubel się przyjmie w społeczności:)

Problem wydajności jest stary jak Hibernate ale niestety jeszcze nie wszędzie uświadomiony. Jako przykład podam tę oto przypowieść.



Niezbyt_dawno, niezbyt_dawno temu w pewnej dużej organizacji strasznie mulił system wykorzystujący Hibernate. Ludzie ze swą wrodzoną skłonnością do tworzenia mitów szybko poradzili sobie z dysonansem poznawczym trapiącym ich mózgi: stworzyli sobie prosty system mitologiczny, który nakazywał im wierzyć, że ORMy tak po prostu mają. Po prostu ich magiczne właściwości powodują, że systemy spowalniają a odczyt danych z bazy jest hamowany jakąś niepojętą siłą. Ciemnotę dodatkowo potęgowała Kasta Adminów Baz Danych, mówiąc: "tak oto kończy się sprzeniewierzanie prastarej składni SQL. Nie chciało się wam niewierni pisać procedur i kwerend to teraz sczeźnijcie w piekle!".


Po wstępnych oględzinach okazało się, że w większości przypadków obarczonych bardzo niską wydajnością mamy do czynienia z klasycznym problemem występującym w ORMach z Lazy Loadingiem: n+1 Select Problem.

Istotę problemy wyjaśnię na przykładzie: Wyobraźmy sobie, że pobieramy listę obiektów klasy Person. Następnie iterujemy po tej liście i dla każdej osoby wołamy getAddresses() po to aby coś tam z adresami każdej z osób zrobić. Jeżeli zapytanie wygląda mniej więcej tak: SELECT p FROM Person p to mam rzeczony problem. Najpierw silnik persystencji wysyła do bazy jedno zapytanie w celu pobrania listy osób. Następnie dla każdej z n osób wysyła zapytanie o jej adresy. W sumie n+1 zapytań:) Pięknie nieprawdaż? Niestety musimy pofatygować się i napisać porządne zapytanie HQL (lub odpowiednie Criteria w Hibernate), które spowoduje dodżojnowanie adresów do osób tak aby wysłane zostało do bazy jedno zapytanie SQL. Niestety nieodzowne będzie dopisanie w HQL paru JOIN FETCH - a co za tym idzie korzystanie z Entity Managera przestaje być takie trywialne jak w tutorialach, pojawia się złożoność, którą warto hermetyzować w DAO/Repozytorium.

Należy uważać również na "utajony" wariant n+1 select problem. W powyższym przykładzie mamy sytuację, w której radośnie iterujemy sobie przy pomocy własnego kodu po osobach i grzebiemy w ich adresach. Może być też tak, że nasz własny kod jest OK, ale niefortunnie podepniemy naszą listę osób pod np jakiś komponent GUI (np h:dataTable w JSF), który ma wyświetlić osoby wraz z adresami i nieszczęście gotowe. Logika renderingu tabelki przeiteruje po osobach aby wyrenderować jej adresy. Wówczas mogą zdarzyć się 2 rzeczy:
- aplikacja się wywali z powodu LazyInitializastionException (lub jakiegoś innego wyjątku w zależności od dostawcy JPA - co zresztą uważam za skandaliczne) ponieważ najpewniej w warstwie widoku sesja będzie już dawno zamknięta
- jeżeli jednak stosujemy jakiś trik typu Open Session In View to wówczas ORM radośnie wygeneruje nam n+1 zapytań do bazy:)))

Pierwszy wariant (wyjątek) jest o tyle dobry, że przynajmniej zwróci czyjąś uwagę podczas devlopmentu i skłoni do napisania porządnego zapytania, które ładuje wszystkie dane w jednym podejściu. Drugi wariant działa więc jest szansa, że nitk nie sprawdzi co się dzieje po stronie bazy:/



Kolejna przypowieść. W tejże samej organizacji istniał inny system, który również niemiłosiernie mulił. Tym razem był to gruby klient do serwera więc lazy loading nie miał zastosowania. Encje przesłane na klienta nijak nie chcą nawiązać kwantowego kanału z procesorem serwera:/ W tym systemie również panowała mitologia. Kiedyś pewien magik podobno zrobił czary mary i LazyInitalizationException nie pojawiał się gdy na kliencie wołano łańcuszki get get get.


Po krótkiej analizie problemu okazało się, że tym razem przedobrzono w inną stronę. Prawie wszystkie powiązania pomiędzy obiektami były EAGER - czyli chciwe lub jak kto woli łapczywe. Skutkowało wyciąganiem połowy bazy przy każdym zapytaniu. O ile serwer bazodanowy dawał radę to wąskim gardłem okazała się transmisja zserializowanych danych. Wniosek znowu ten sam: niestety na skróty się nie da - trzeba się pofatygować i napisać porządne zapytania specyfikując co w jakim Use Case ma być podciągnięte. Niestety zakorzeniona mitologia i szargana opinia o ORMach pozostanie w organizacji na parę następnych pokoleń.

Przy okazji warto wspomnieć o kolejnej pułapce jaką szykuje na nas maper. Uwaga na powiązania typu n-1 lub 1-1 - czyli takie gdzie w klasie encji mamy pole klasy innej encji (np Person ma jeden Adres). Standard JPA zakłada, że wówczas powiązanie jest typu EAGER. Czyli pobierając z bazy osobę zawsze od razu wyciągniemy jej adres. Niestety odbywa się to zazwyczaj w osobnym zapytaniu. Czyli w przypadku pobierania listy osób mamy n+1 Select Problem:)


Żeby nie było, Lazy Loading może również przydać się w celu optymalizacji systemu.
Wyobraźmy sobie taki przypadek: Mamy klasę dokument, która zawiera szereg pół - w tym pole zawierając treść dokumentu: kilkadziesiąt stron A4 (pomińmy kwestię sensowności takiego projektu). Gdy chcemy zaprezentować listę dokumentów na GUI, która zawiera dajmy na to tytuł i datę to oczywiście zupełnie niepotrzebnie będziemy pobierać z bazy i przechowywać w aplikacji (choćby chwilowo) dziesiątki megabajtów tekstu. Jednym z paru rozwiązań ale najprostszym jest ustawienie leniwego ładowania kolumny z treścią. Gdy nie będzie potrzebne wówczas nie będzie ładowane z bazy.


//============================

Podstawą pracy z ORM jest konsola z logami. Podczas developmentu należy włączyć wyświetlanie generowanych poleceń SQL i nieustannie obserwować oczami swymi logi silnika ORM czy przypadkiem nie pojawia się na niej w pewnym momencie kilka/-dziesiąt/-set/-tysięcy zapytań. Oczywiście n+1 Select Problem możemy zauważyć na konsoli praktycznie tylko wówczas gdy napełnimy bazę większą ilością danych. Dla n=1 problemu raczej nie uraczymy.

W dobrym procesie produkcji softu jest czas na takie coś jak kontrola kodu - przeglądanie nowego kodu przez bardziej doświadczonych członków teamu (lub chociaż przez drugą parę oczu). Co powiecie na kontrolę konsoli? Pokaż misiu jakie zapytania do bazy generuje Twój kod.

Podsumowując dwie przytoczone przypowieści pozwolę sobie zacytować Franka Zappa: "Głupota ma pewien urok, ignorancja nie".

niedziela, 19 kwietnia 2009

DAO a sprawa Lazy Loading



Wracając do wczorajszego tematu DAO rozwinę nieco kontekst mapowania relacyjno-obiektowego na przykładzie standardu JPA. Dokładnie - skupię się na głównym ficzerze każdego mapera zgodnego z JPA, czyli tytułowym leniwym (opóźnionym) ładowaniem zagregowanych w encji obiektów.

Niektórzy pewnie zastanawiają się czego znowu ten zgred się czepia. Przecież LL to taki fajny ficzer. Mamy sobie załadowany z bazy obiekt (i jeżeli sesja kontekstu perzystencji nie została zamknięta) to wystarczy zawołać na nim getXXX(), czary mary i pobrany przez getter zagregowany obiekt został podciągnięty z bazy oraz zapakowany do encji.

Wszystko fajnie, ale jeżeli korzysta się z tego z świadomie i w odpowiednim momencie. W przeciwnym wypadku mamy spore problemy z wydajnością. Ale o tym za w następnym poście. Ja mam z tym jeszcze pewien problem natury filozoficznej, który pojawia się gdy do naszej architektury wprowadzimy warstwę DAO (bez której jak bez ręki - co chciałem wczoraj wykazać)...

Zatem weźmy przykładowy interfejs DAO:


public interface PersonDAO{
Person getPerson(Long id);
}


Standard... zwykłe DAO (nie czas teraz na Repozytorium z DDD), zwykły klucz (nie czas teraz na UID czy klasę kluczą w dobrym stylu DDD).

Dalej będzie oczywiście jedna z możliwych implementacji w JPA:


public class JpaPersonDAO implements PersonDAO{
EntityManager em;
Person getPerson(Long id){
return (Person)em.find(Person.class, id);
}
...
}


Też standard, bez żadnego generic DAO czy innych klas bazowych, bez wnikania w szczegóły wstrzykiwania EM. Nie w tym rzecz, chodzi tak jak wspomniałem o problem natury filozoficznej, mianowicie:
Co ja mam napisać w komentarzu metody getPerson() w interfejsie? (i od razy mówię, że banał typu "return returns person that has ID relevant do given id" mnie nie zadowala)

Aby w komentarzu napisać prawdę, całą prawdę i tylko prawdę musiałbym popełnić coś takiego (po polsku będę komentarz pisał - wybaczcie):

"
Metoda zwraca osobę o zadanym ID. Drogi programisto-użytkowniku klas implmentujących ten interfejs, wiedz ,że niniejszy interfejs zawirea z tobą kontrakt o następujących warunkach:
1. O ile korzystasz z implementacji DAO opartej o JPA to możesz się spodziewać tego, że:

1.a Niektóre klasy obiektów zagregowanych w zwróconej instancji osoby zostaną zastąpione przez klasy specyficznych pośredników (np PersistentBag w hibernate) więc niniejszy interfejs nie może zagwarantować Ci poprawnej pracy w środowisku rozproszonym (np aby wynik działa tej metody przesłać na zdalnego klienta musisz mu zapewnić odpowiednie biblioteki w odpowiednich wersjach).

1.b Być może masz dostęp do obiektów zagregowanych w zwróconej instancji. Na przykład do adresu (przez metodę getAddress). Zależy to od tego czy znajdujesz się w miejscu, w którym jeszcze obowiązuje sesja kontekstu persystencji. Jeżeli sesja będzie zamknięta, a ty będziesz brzydko się bawił z instancją person to dostaniesz np LazyInitializationException - ale tego w tym momencie nie mogę zagwarantować, gdyż zależy to od dostawcy (specyfiakcja JPA nic nie narzuca w temacie reagowania na zamkniętą sesję). Niestety niniejszy interfejs jest czystą abstrakcją dostępu do danych i nie mogę Ci zdradzić szczegółu na jakim dostawcy JAP będzie oparta implementacja tego interfejsu.

2. Jeżeli implementacja tego interfejsu nie będzie oparta o JPA (a np o dostęp do web service albo pliku XML) to wiedz, że dostępu do żadnych zagregowanych obiektów nie masz.

return osoba o zadanym ID, z którą ni cholery nie wiadomo co można na 100% bezpiecznie zrobić.
"


Ogólnie chodzi mi o to, że staroświeckie DAO i nowoczesny mechanizm Lazy Loadingu mają się nijak do siebie.

//==================================

Mamy tu do czynienia z bardziej ogólnym problemem: przeciekającą abstrakcją. Szczegóły techniczne mappera relacyjno-obiektowego (takie jak lazy loading, problem z wygasłą sesją) literalnie wyciekają z interfejsu DAO, który powinien być czystą abstrakcją dostępu do danych niezależną od jakiś maperów. Innymi słowy klient (kod) interfejsu DAO musi być niestety świadomy pewnych "wyciekających" mechanizmów stojących za jedną z jego implementacji. Nie może jej traktować jako czarnej skrzynki, która robi to co ma robić - i robi to dobrze.

sobota, 18 kwietnia 2009

DAO



Jak donoszą brukowce DAO odeszło i zostało zastąpione przez JPA. Czy aby na pewno?

Piotrek w swym poście (będącym niezłym zbiorem argumentów w dyskusji) wyraził zdziwienie pojawiającymi się od pewnego czasu głosami przeciwko DAO. Natomiast ja chciałbym dodać od siebie dodatkowo nieco typowej dla siebie irytacji spowodowanej wąskim spojrzeniem na problem pojawiającym się w głosach autorytetów z wielkiego świata.

Problem potrzeby lub nie warstwy DAO nie jest oczywisty i zależy od kontekstu. Czasem DAO jest niezastąpione a czasem może lekko przeszkadzać. Ale na pewno pozbawione kontekstu autorytarne głosy typu "DAO bad" są bez sensu.


DAO ma na celu separację logiki dostępu do danych od logiki biznesowej/aplikacji.
Co nam to daje?
Szeroką i sensowną listę podał Mario Gleichmann w swoim poście.

Ja ograniczę się do paru z nich:

  • wynikającą z separacji możliwość podmiany sposobu dostępu do danych
  • wynikający z możliwości podmiany zwiększony poziom testability
  • reużywalność logiki dostępu do danych


Przyjrzyjmy się zasadności wylistowanych argumentów:
Podmiana sposobu dostępu do danych - teoretycznie silny argument; praktycznie bezużyteczny. Jak często zdarzyła się Wam taka sytuacja? O ile bardzo często zmienią się (no w zależności od klienta) wariacje logiki biznesowej to zmiana źródła danych? O ile zmienną logikę biznesową warto hermetyzować pod stabilnym interfejsem i jej wariacje enkapsulować we wzorcu strategii to robienie tego z dostępem do danych wydaje się nakładaniem gaci przez głowię. Chyba, że...

Testowanie - separacja dostępu do danych od logiki biznesowej zwiększa dramatycznie poziom testability, czyli podatność kodu na testy. Tak podatność na testy - ponieważ nie każdy kod można łatwo testować.
Jeżeli mam osobną klasę realizującą jakąś funkcjonalność biznesową oraz osobną klasę DAO odczytującą/modyfikującą dane to wówczas:
- możemy poddać testom sam "komponent" DAO - co się oczywiście będzie rzadko zdarzać, więc ten argument się nie liczy;P
- możemy testować samą logikę biznesową - na czas testów nasza logika może używać innej implementacji DAO (np takiej, która nie korzysta z bazy danych). Co na to daje?
Testy logiki biznesowej są szybsze ponieważ nie tracimy czasu na operacje zapisu odczytu danych (których poprawność w tym momencie nas nie interesuje). Czas zapisu/odczytu potencjalnie może być stosunkowo długi, wystarczy wyobrazić sobie źródło danych, które jest bazą zawierającą miliony rekordów, gigantycznym plikiem, czujnikiem chemicznym, web servisem wykonującym się średnio 5 min. Zamiast 100000 testów na godzinę, możemy ich wykonać tylko 10 gdy będziemy ciągnąć za sobą bagaż dostępu do danych.

Reużywalność - posługiwanie się wprost EntityManagerem w kodzie biznesowym do pobierania encji (tak jak chcą tego piewcy śmierci DAO) jest pomysłem arcy-chromym.
Kod w stylu
em.find(Person.class, id)
nie wygląda groźnie - nieprawdaż? Niestety jest on jednak typowy dla tandetnych turoriali na poziomie Hello World. W rzeczywistości jednak piszemy kwerendę ponieważ:
- mamy dynamiczne zapytanie (np doklejamy WHERE person.name = 'xxx' o ile user wpisał imie, itp)
- chcemy dociągnąć chciwie zagregowane obiekty (i nie życzymy sobie lazy loadingu gdyż nierzadko JEST ZŁY - o czym będzie w następnym poście)

Nawet jeżeli w pierwszej iteracji nasze pobranie danych jest prostym find() to zwykle za miesiąc zmieni się w kwerendę. Nie chcemy zatem programować metodą Kopiego-Pasty. Dzięki istnieniu DAO możemy reużywać złożoną logikę zapytań oraz oczywiście zmieniać je w 1 miejscu.

Separacja dla mnie osobiście separacja kodu - choćby zapytania były trywialne albo totalnie niereużywalne jest wystarczającą zaletą przemawiającą za DAO.


Potrafię sobie wyobrazić, że żadne z powyższych uwarunkowań nie występują. Tzn:
- nigdy nie zmieniamy źródła danych - co jest naturalne
- nie testujemy kodu - nie oszukujmy się, taka jest rzeczywistość
- nie mamy złożonych zapytań i logiki sklejania HQLa

Wówczas możemy radośnie używać EntityManagera w kodzie biznesowym. Sugerowałbym jednak kompromis: wystarczą osobne klasy realizujące logikę dostępu do danych - bez abstrakcji: bez interfejsu, bez wstrzykiwania.



Wąski kontekst
W bardzo prostych aplikacjach trzymanie się ściśle architektury z warstwą DAO niesie ze sobą pewnie narzuty i utratę prostoty. Mam tu na myśli niezbyt ambitne ale bardzo potrzebne na rynku i popularne aplikacje typu przeglądarka bazy danych. Po prostu łatwiej i taniej jest wyprodukować za 1 mln zł system, który w przeglądarce wyświetli tabelkę z listą kalesonów w hurtowni niż postawić samą bazę danych i przyuczyć pracowników z podstaw SQL;)))

W tego typu systemach zwykle corowa funkcjonalność to ekrany z różnego rodzaju listami + kontrolki do wpisywanie kryteriów + button SZUKAJ. Do tego ekran podglądu i edycji.

Najbardziej produktywne jest wówczas podejście minimalistyczne. Warstwa prezentacji tworzy obiekt Criteria i przesyła go niżej do wykonania. Przekombinowane podejście polegające na tworzenie warstwy DAO, która abstrahuje źródło danych zmusza nas również do abstrahowania od kryteriów wyszukiwania. GUI musi wysyłać jakieś DTO, które w DAO jest przepakowywane w Criteria. Generalnie: nakładanie gaci przez głowę.

Szerszy kontekst
Dla kontrastu rozważmy hipotetyczny przykład skomplikowanego dostępu do danych.
Wyobraźmy sobie, że w naszym systemie mamy użytkownika przechowywanego w 1 bazie danych. Z niewiadomych przyczyn jego zamówienia są przechowywane w innej bazie. Adresy tegoż użytkownika są pobierane z zewnętrznego systemu przez jakiś WebService.
Załóżmy też, że w zależności od wdrożenia druga baza danych (ta z zamówieniami) oraz źródło adresów są zmienne.

Podążając za DDD stworzylibyśmy agregat User oraz repozytorium UserRepository, które jest w stanie go zebrać i poskładać do kupy. Repozytorium mogłoby używać EntityManagera do pobrania encji User oraz dwóch DAO do pobrania zamówień oraz adresów z abstrakcyjnych źródeł. Problem dostępu do danych może zawierać w sobie również aspekt przepakowania obiektów z jednej domeny (np z innego systemu) do domeny innej.


Przykład powyższy może wydawać się nieco wydumany i przesadzony, ale w rzeczywistości tego typu spawy istnieją i nie należy o tym zapominać. Chciałem tylko pokazać, że architektura dostępu do danych wcale nie jest trywialnym problemem jak przedstawia się to w niektórych materiałach lub (o zgrozo!) przez niektórych członków grup eksperckich JEE. Owszem często taki jest. Ale spektrum problemów jest szerokie i po specyfikacji platformy korporacyjnej spodziewalibyśmy się czegoś więcej.

//==================================

Na zakończenie pamiętajmy aby dobierać sobie młotek do problemu, a architekturę dostępu do danych do... też do problemu:)

Sentencja Alberta Einsteina "Wszystko powinno być tak proste, jak to tylko możliwe, ale nie prostsze" może wydawać się trywialna, ale często o niej zapominamy.

A do implementacji większości systemów i tak nie potrzeba żadnego JPA - wystarczy Excel + jakieś cwane makro... no może Access ostatecznie;P