czwartek, 3 września 2009

Lazy loading w kontekście paradygmatu Command-query Separation



ODŚWIEŻENIE KONTEKSTU

Kilka miesięcy temu popełniłem dwa posty na temat Lazy loadingu (wiem, że nie chce się Wam ich czytać więc streszczam):

Lazy Loading a sprawa wydajności - post traktujący ogólnie o drastycznym spadku wydajności w sytuacjach gdy LL jest stosowany w niewłaściwym momencie.

Chodzi głównie o klasyczny "n+1 Select problem", który potrafi totalnie zamulić system. W skrócie: problem pojawia się gdy pobieramy z EnityManagera/Sesji hibernate kolekcję a następnie iterujemy po niej i getterami pobieramy zagregowane obiekty. Wówczas do bazy wysyłane jest 1 zapytanie o kolekcję oraz dla każdego z jej n elementów koleje zapytania dociągające potrzebne obiekty.

Podobne schorzenie występuje gdy naiwnie ustawimy w mapowaniu FetchMode na EAGER.

DAO a sprawa Lazy Loading - post poruszający problem tak zwanej cieknącej abstrakcji, czyli "brudzenia" kodu logiki warstw wyższych szczegółami technicznymi typu zamknięta sesji persystencji.

Oczywiście mamy sprawdzony sposób na cieknącą abstrakcję - podejście Open Session in View (rozwiązanie w Springu np przez Mateusza Mirackiego). Niestety w tym przypadku łatwo dopuścić do opisanego powyżej "n+1 Select Problem". Ciekawe rozwiązanie z testowaniem ilości wysyłanych do bazy zapytań przy pomocy statystyk Hibernate zaproponował w komentarzu do mojego posta Milus.


CREDO

Od razu śpieszę wyjaśnić - żeby nie było, że jestem jakimś fanatycznym wrogiem Lazy loadingu czy ORM w ogólności. Wręcz przeciwnie, namiętnie go stosuję od 5 lat i uważam za bardzo wygodny młotek w większości sytuacji.


PROBLEM
Od czasu opublikowania tych postów dostałem kilka maili z zapytaniami o sposoby radzenia sobie z powyższymi problemami.

Pod postami wywiązały się też krótkie dyskusje prezentujące możliwe podejścia.

Właściwie to chodzi głównie o problemem z wydajnością, ponieważ filozoficzny problem cieknącej abstrakcji prawie nikogo nie boli. Nauczyliśmy się z nim żyć i raczej się nad nim nie zastanawiamy (na szczęście Jacek podziela moje rozterki).

PRAWDZIWY PROBLEM
Wg mnie prawdziwy problem leży w samym podejściu do dostępu do danych, czyli architekturze aplikacji. Problem polega na zbytnim uogólnieniu.

Tak samo traktujemy dwa diametralnie różnie rodzaje obiektów:
- obiekty domenowe, które wykonują operacje biznesowe (lub na których to wykonujemy modyfikacje w podejściu proceduralnym)
- dane "przekrojowe" potrzebne jedynie do prezentacji (np wyświetlenia na GUI) wycinka aktualnego stanu systemu

CQS
Teraz nadszedł wreszcie czas na przedstawienie tytułowego bohatera tego posta: zapomniany i zakurzony paradygmat: Command-query Separation.

Paradygmat tez zakłada, że system posiada "interfejs", przez który wysyłamy do niego polecenia oraz osobny, przez który odpytujemy o dane. Nigdy nie projektujemy operacji, które zarówno coś modyfikują jak i odczytują dane.

W jaki sposób możemy wykorzystać to podejście w systemach enterprise?
"Interfejsem" przez który klienty (nie klienci) komunikują się z systemem może być warstwa aplikacji. Cienka warstwa, która zajmuje się wszystkim, oprócz logiki biznesowej i dostępu do danych.






COMMAND
Ok, chcemy coś zrobić w systemie, wysyłamy do niego Polecenie. Tak jak wspomniałem "interfejsem" jest warstwa aplikacji "opublikowana" jako jakieś bezstanowe servisy lub obiekty stanowe - zależnie od wymagań.
Jeżeli klient wyśle do tej warstwy Command, wówczas pobiera ona z Repozytorium jakieś encje (lub agregaty w DDD). Dalej na encjach/agregatach uruchamiamy ich metody biznesowe - jeżeli bawimy się obiektowo, lub wywołujemy jakieś biznesowe servisy przekazując im jako parametry pobrane właśnie encje. Nic specjalnego, klasyczna architektura warstwowa.

Natomiast w tym właśnie przypadku - gdy wysyłamy do systemu Command mający zwykle na celu wykonanie jakiś operacji biznesowych - jak najbardziej możemy (ba powinniśmy) radośnie korzystać z Lazy loadingu. Jest to jak najbardziej właściwy moment ponieważ natura takich operacji jest zwykle taka, że pobieramy kilka obiektów, które wchodzą ze sobą w jakąś interakcję (lub władają nimi servisy). Rzeczone obiekty biznesowe ewentualnie potrzebują do wypełnienia swej biznesowej odpowiedzialności zagregowanych składników. Raczej nic złego się nie stanie, gdy zamiast 3 prostych zapytań do bazy wyślemy ich 5 czy nawet 10.
Czasem nawet będzie to bardziej wskazane niż join - zależy do natury danych.

Cała operacja wykonuje się w obrębie metody z warstwy aplikacji więc jest objęta transakcją i ma cały czas otwartą sesję persystencji więc nie martwimy się o wyjątki Lazy loadingu.


QUERY
Natomiast jeżeli do systemu trafia Query, czyli zapytanie o dane, to wówczas sprawa wygląda nieco inaczej...
Przede wszystkim warstwa aplikacji nie ma pod sobą warstwy z logiką (żadnych servisów biznesowych). Czy potrzebujecie abstrakcji dostępu do danych (DAO/Repozytorium)? Raczej nie zmienicie nigdy źródła danych. Nie ma sensu również testowanie jednostkowe "Finderów" aplikacyjnych z podmienionymi na mocki DAO. Czyli czyste pobieranie danych.

W mniej złożonych systemach możemy sobie pozwolić na zwrócenie encji w odpowiedzi na kwerendę.
W bardziej poważnych raczej nie możemy pozwolić sobie na ujawnianie klientom naszego modelu, więc zwrócimy jakiś Data Transfer Object (DTO). Hermetyzacja modelu to podstawa - dzięki temu może on ewoluować iteracyjnie bez obaw o zniweczenie pracy teamu dospawującego prezentację.


QUERY ZWRACAJĄCE ENCJE
Jeżeli zdecydujemy się na zwracanie encji to musimy uporać się z paroma problemami technicznymi:
- wydajność: zwykle scenariusz obsługi kwerendy to pobranie kolekcji danych. Wówczas mamy jak w banku opisany na wstępie "n+1 Select problem". Rozwiązanie jest bardzo proste - wystarczy się pofatygować i napisać zapytanie z klauzulą JOIN FETCH. Przykładowo SELECT p FROM Person p JOIN FETCH p.addresses - dzięki temu chciwie/łapczywie (nie wiem, na które rozkoszne tłumaczenie się zdecydować) pobierzemy osoby wraz z podciągniętymi adresami. Po prostu ORM wygeneruje SQLa z JOINem.

Niezbyt dobrym pomysłem jest ustawienie w mapowaniu powiązania obiektów z FetchMode.EAGER. Spowoduje ono, że zawsze wyciągając jeden obiekt pobierzemy jego "dziecko". Owszem są sytuacje, gdzie z kontekstu biznesowego takie podejście jest sensowne, ale zwykle stanowią zdecydowaną mniejszość. Zwykle w jednym Use Case zależy nam na pobraniu np osób z adresami a winnym adresy są zbędne.

Warto pamiętać, że domyślnie strategia EAGER obowiązuje dla powiązań wiele-jeden, jeden-jeden i warto ją wyłączać.

- Open Session in View - podejście to o ile jest wygodne to niestety pozwala łatwo zapomnieć o tym, że leniwie podciągamy jakieś dane. Po prostu istnieje niemała szansa, że na widoku odwołamy się do adresów osoby a w zapytaniu zapomnimy dopisać JOIN FETCH. Działa? Działa. Muli? W środowisku developerskim z małą ilością danych pewnie nie;P

- Ilość danych - zwykle Use Case gdzie do systemu trafia Query zakłada, że z bazy trzeba pobrać dane "przekrojowe". Czyli dane z wielu tabel, ale z każdej z nich interesuje nas zaledwie kilka kolumn. W małych systemach, gdzie warstwa GUI i warstwa aplikacji stoją na tej samej JVM będzie to w śmigać.

Ale nawet w takiej konfiguracji mamy problem z pobieraniem zbędnych danych. Przykładowo: gdy pobieramy z bazy np dokumenty aby jedynie wyświetlić ich listę (data, autor) a każdy z nich ma kolumnę przechowującą dziesiątki stron textu. Rozwiązaniem jest leniwe ładowanie pól - czyli ich nieładowanie:) W hibernate wymaga to poddanie skompilowanego bytecodu instrumentalizacji.

Innym podejściem może być zamapowanie tabeli przez kilka klas. Przykładowo DocumentFull, DocumentLight, itp... Jeżeli czujecie niesmak na myśl o mnożeniu bytów to nie jesteście sami.

Hibernate pozwala na pobieranie danych wprost do DTO. Tworzymy DTO szyte na miarę danego Use Case, a składnia wygląda tak:
SELECT new pakiet.KlasaDTO(pole1, pole2.podpole) FROM...
Oczywiście przy założeniu, że odpowiedni konstruktor istnieje.

QUERY ZWRACAJĄCE DTO
Jeżeli zdecydujemy się na zwracanie DTO to zapewne dlatego, że potrzebne dane są na tyle przekrojowe, że żaden zestaw encji nie modeluje ich sensownie (i optymalnie).
Innym powodem może być chęć hermetyzacji zmiennego modelu poza stabilną anticorruption layer. Warstwa zapobiegająca gniciu to pojęcie z DDD i ma pragmatyczny sens w nieco bardziej perspektywicznych projektach.

Poza tym możemy być dumni, że nawet nasza architektura wspiera Agile umożliwiając ewolucję modelu domenowego bez rujnowania wszystkiego dookoła:)

Częstym błędem w tym przypadku jest pobieranie z ORM encji a następnie przepakowywanie ich w DTO.
Nie tędy droga...


Pobierzmy tylko to co tak na prawdę jest potrzebne. Najprościej zrealizować to z użyciem wspomnianej konstrukcji Hibernate SELECT NEW. Jednak w złożonych systemach zwykle nieodzowny będzie co najmniej w paru jakiś szyty na miarę i zoptymalizowany SQL. Jakąś abstrakcją nad SQLem może być wówczas np iBATIS mapujący result na DTO.


Jak to zwykle bywa najlepsze będzie podejście hybrydowe. Tam gdzie możemy na to sobie pozwolić zwracamy w wyniku obsługi Query encje - zwiększając tym samym swą produktywność. Natomiast tam gdzie krytyczna jest wydajność lub specjalna struktura danych, zwracamy DTO.


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

Opisane powyżej aspekty wydajności związane z Lazy Loadingiem aplikują się do systemów każdej wielkości - szczególnie "n+1 Select problem". Natomiast rozwiązanie z CQS a szczególnie podejście gdzie zwracamy z kwerendy DTO jest bardziej pracochłonne, przez co aplikuje się do projektów bardziej perspektywicznych.

Ale czy jest sens tworzyć w JEE projekty inne niż perspektywiczne? Do prostych i szybkich zadań typu "przeglądarka do bazy" jest przecież Microsoft Access;P

23 komentarze:

Antoni Jakubiak pisze...

Ważna rzecz, przy zwracaniu encji należy zwrócić szczególną uwagę na względy bezpieczeństwa...
Na przykład, klasa User( String login, String password ) - a gdzieś dalej trafia to w całości do XML przez AJAX.
Bezpieczeństwo, to jeden z głównych powodów dla których nie zapomnimy prędko o DTO.
Z kolei samo DTO to okropny wzorzec projektowy. Kopiowanie kodu z jednego obiektu do drugiego jest strasznie brzydkim rozwiązaniem i generuje błędy zwykle wtedy, gdy aplikacja jest rozbudowywana.
Wróćmy do klasy User i problemu password. Nie chcemy, aby password było przekazywane do warstwy prezentacji - na przykład do jakiegoś AJAX lub FLEXa. Powinniśmy stworzyć klasę UserWithoutPasswordDto i mozolnie kopiować pola lub skorzystać z jakiegoś mappera DTO gdzie deklarujemy schemat kopiowania danych.
Jeszcze ciekawszy przypadek, to klasa Pracownik i pole zarobki. To na tyle ciekawy przypadek że nie zmieści się w żadnym komentarzu.

Maciej Zubala pisze...

"Z kolei samo DTO to okropny wzorzec projektowy. Kopiowanie kodu z jednego obiektu do drugiego jest strasznie brzydkim rozwiązaniem i generuje błędy zwykle wtedy, gdy aplikacja jest rozbudowywana."

Z tego co ja zrozumiałem to Sławek proponuje nie przepakowywać encji do dto, tylko od razu pobierać dane z b.d. do dto. W HQL można robić w ten sposób:

select new FamilyDTO(mother, mate, offspr)
from DomesticCat as mother
join mother.mate as mate
left join mother.kittens as offspr

Sławek Sobótka pisze...

Tak Maćku, chodzi o to żeby pobierać to co jest potrzebne wprost do DTO. W najprostszym przypadku da się to zrobić właśnie w HQL przy pomocy konstrukcji SELECT NEW DtoKonstruktor(..).

Przepakowanie z encji mija się z sensem gdyż i tak wyciągamy tony niepotrzebnych danych z bazy a później chwilowo trzymamy je w pamięci.

Co do bezpieczeństwa to dodałbym jeszcze jeden ważny aspekt, o którym zapomniałem napisać: operacja kaskadowe. Zwykle są one włączone gdzie się da dla naszej wygody...

Ale w naiwnym podejściu daje to możliwość taką: zapisuję zamówienie dla danego usera; w zamówieniu jest zatem podpięta encja usera. Klient może sobie pod tego usera podpąć kolekcję ról/uprawnień (czy czegokolwiek w zależności od modelu) i wysłać do servisu w celu zapisania.

Oczywiście w systemach z grubszym klientem jakiś maper typu flex-java może ignorować pewne atrybuty usera. Ale przykładowo w systemach z cienkim (typu JSF) musimy już pilnować zespół czy przypadkiem nie robi czegoś dziwnego (nie mówię, że w złej wierze, ale po prostu nieświadomie) w warstwie GUI.

Po prostu brak hermetyzacji mści się w najmniej spodziewanym momencie;P

milus pisze...

Witam

Ja jednak cały czas upierałbym się na tworzeniu interfejsów dla obiektów domenowych.
Oczywiście w bardzo specyficznych przypadkach - myślę o zapytaniach a'la raportowych, w których to wyciągamy szczyptę danych z wielu tabel, to tworzenie DTO jest jak najbardziej poprawne/wygodne/użyteczne. A jak jeszcze korzystamy z dobrodziejstwa hibenate, aby wypełnił nam sam takie DTO to tymbardziej super...
Jednak w bardziej standardowych przypadkach, korzystając z IDE który potrafi za pomocą kliknięcia utworzyc interfejs z klasy domenowej. Oczywiście w bardziej skomplikowanych przypadkach ilośc interfejsów reprezentujących dane zwracan do GUI będzie rosnącm ale z mojego doświadczenia rośnie wolniej niż ilośc DTO.
Tworzenie "szytych na miare DTO" które są automatycznie wypełniane przez Hibernate ma jeszcze jedną zasadniczą dla mnie wade: strasznie ciężko się to refaktoruje i utrzymuje:
-konstruktory potrafią miec multum parametrów i czytanie tego jest koszmarem
- nazwy propertiesów w zapytaniu zaszyte są w postacie stringów

O wiele lepiej w takim przypadku spisuja się ResultTransformery, ale z tego co pamiętam to chyba nie do konca mozna nimi oporządzic powiązane kolekcje, zresztą nie wiem czy ten sam problem nie dotyczy "select new ".
Jeśli się mylę to bardzo proszę o poprawienie.

Sławek Sobótka pisze...

Tak, rzeczywiście "programowanie na stringach" jest przykre ponieważ tak jak napisałeś jest nierefakoryzowalne. Czytelność kodu klas, które mają 20 parametrów w konstruktorze jest też niska.

Interfejsy dla obiektów domenowych...
hmmm mnie osobiście ten pomysł jakoś nie leży ponieważ jak dla mnie jest to konstrukcja służąca polimorfozmowi a nie ograniczaniu widoczności. Ale tutaj nie będę dyskutował, ponieważ wynika to jakieś tam mojego ortodoksyjnego podejścia do OO.

Pomijając to, interfejsy zapewnią nam "bezpieczeństwo formalne", natomiast w rzeczywistości klient i tak dostaje bajty z danymi, o których w danym momencie nie powinien wiedzieć - zatem zawsze może sobie grzebać tam gdzie nie powinien.

Ale nie traktuję tego argumentu jako kontrargumentu ponieważ jest dosyć paranoiczny i w znakomitej większości przypadków pomijalny.

Poza tym interfejs nie zabezpieczy w żaden sposób przed "n+1 select problem" ponieważ jeżeli ktoś nie dopisze JOIN FETCH do atrybutu a interfejs ma do niego getter to nieszczęście gotowe.

Tak samo nie unikamy pobierania zbędnych danych z bazy.
Ale podkreślę jeszcze raz: nie zawsze jest to bolący problem. W jakieś klasie systemów jest to pomijalne.

milus pisze...

No coż ja dochodzę do tego bardziej pragmatycznie a poza tym to że zwracasz interfejs a nie faktyczną klase uważam że jest jak najbardziej zgodnie z paradyngmatem OO ! Bo poszczególne elementy twojego systemu zależą od interfejsu a nie od faktycznej implementacji...
W razie potrzeby przeciez możesz podmienic implementacje.
Zresztą w moim ostatnim projekcie już to się zdarzyło: zwracałem interfejs którym był zaimplementowany przez klase domenową, ale póżniej okazało się że pewne dane trzeba ciągnąc skąd indziej = > stworzyłem dodatkowe DTO,które implementowało interfejs od którego było zależne GUI...

Wracając do bezpieczeństwa: jeśli będziemy nasze dane transportowac przez FLEX to faktycznie takie rozwiązanie może okazac się dziurawe... Ale jeśli korzystamy z JSF/JSP i innych takich to jeśli ktoś nie będzie nam robic w wartswie WEB castowania to wszystko jest czyste.

Oczywiscie "there is no free launch" i te interfejsy trzeba tworzyc (w całoście można to robic przez klikniecia w IDE), utrzymywac (to juz niestety sami) no i także musimy testowac czy cały graf obiektów opisany przez interfejs jest zainicjowany.
W tym ostatnim przypadku z powodzeniem korzystam z Spring TestContext Framework.

A wiesz może jak wygląd "select new(...)" gdy chciałbys załadowac jednocześnie usera i jego wszysktie adresy ?

Sławek Sobótka pisze...

Z kolekcją jest problem:/
Jeżeli sytuacja pozwala można przekazać obiekt który ma w sobie tą kolekcję i wówczas w konstruktorze:

JakisKonstruktor(Klasa obiekt){
kolekcja = obiekt.getKolekcja();
}


Można próbować też użyć transformera:
createQuery("select c as c, c.name as name from Customer c").setResultTransformer(Transformers.aliasToBean(YourClass.class));


Co do paradygmatu OO:
czy tworzenie interfejsu (najczystszej formy abstrakcji) dla czegoś co abstrakcyjne nie jest (adres, klient) ma sens?
Wiem, że ma zastosowanie w celu ominięcia jakiś tam niezręczności technicznych, ale czy ma sens w kontekście modelu domeny problemu?

Zresztą... będąc w kontekście encji, które nie są zgodne z zasadą hermetyzacji i w standardowym podejściu nie mają żadnej odpowiedzialności nie warto spierać się o OO;P

milus pisze...

W sprawie kolekcji w "select new ":
nie jestem specjalista w tym, poniewaz nie jest to technika ktora czesto wykorzystuje, ale z tego co pamietam to dla kolekcji to faktycznie jest ciezko. Hibernate w "select new" spłaszcza
wyniki ( tak jak SQL)
Pozatym jeśli nawet pzekażesz obiekt w którym masz dostęp do kolekcji to sprowadzi sie do tego ze zwrocisz DTO z zapakowaną kolekcją obiektów domenowych - czyli coś co od poczatku chcielibyśmy uniknąc...


A jeśli chodzi o rozważąnia na temat OO, to dryfujemy coraz bardziej w strone akademickich rozwazań, ale dla mnie definiowanie zależnośc pomiędzy warstwami przy użyciu interfejsów jest jak najabardziej słuszne.
A to że zdefiniujesz,że te abstrakcje są definiowane(implementowane) przez obiekty domenowe jest pragmatycznym podejsciem do sprawy.
Zauważ że wychodzę tu z założenia,że te interfejsy definujace kontrakt między warstwami nie muszą byc wcale implementowane przez obiekty domenowe. Pewne pragmatyczne podejście mnie pcha do tego aby obiekty domeny implementowały ten kontrakt :)

Sławek Sobótka pisze...

Nie sprawdzałem tego, ale może da się wyciągnąć parę atrybutów tegoż obiektu + zawartą w nim kolekcję. Tak jak c.name we wklejonym snippecie.

Pytanie tylko czy tak na prawdę chcemy wyciągnąć z bazy kolekcję tak jak jest zamapowana czy jedynie niektóre kolumny - tak jak w encji zawierającej tą kolekcję:)

Brałem kiedyś udział w tworzeniu całkiem dużego systemu: 1200 tabel (domena to było ok 800, reszta: legacy syf) i proces wyglądał tak:
najpierw wszystkie zapytania zrobiliśmy w hibernate - pierwszy release systemu. Później stopniowo przepisywanie tego co muli na native SQL. Ostatecznie ORM służył głównie do modyfikowania danych (CUD), natomiast złożone R to były kwerendy/procedurki SQL.

Odnośnie zależności (pure/strict OO to mój konik): zależności to nie zawsze zuooo. Niektóre z nich są naturalne ponieważ wynikają z natury problemu, inne są wręcz rażące. Architektura - rozumiana jako ogólny styl projektowania - również definiuje, które są zuee a które nie.

Jeżeli mamy model domenowy to jak najbardziej powinien być hermetyczny w obrębie modułów systemu. Jeżeli jeden moduł korzysta z drugiego to nie powinien nic wiedzieć o jego modelu. Tu częstym błędem to tworzenie "enterprise model" - wspólne monstrum dla całego systemu.

Zależność pomiędzy warstwą aplikacji a prezentacji: w kanonicznym podejściu oczywiście jej być nie powinno ale często w małych systemach wypycha się encje na gui i binduje z formularzem. Ok, pragmatyzm dostosowany do problemu danego systemu.

I teraz jeżeli chcemy poluźnić powiązania warstwy prezentacji z niższą poprzez interfejsy na encjach to co nam to daje.

Jeżeli encja ma metodę getXXX i na gui jej potrzebujemy to interfejs również będzie ją posiadał. Jeżeli teraz model się zmieni i już nie będzie miał XXX to wówczas aby wciąż spełnić kontrakt narzucony przez interfejs co zrobi encja? Odwoła się do DAO/Repo? Raczej nie, więc wówczas zmiana zajdzie tak na prawdę w jakimś servisie, który zwracał te interfejsowane dane.

Zatem dochodzimy do kuriozum: Encja jest "pobrudzona" interfejsem, ale w razie zmian zmienia się servis. Servis zwróci już pewnie jakieś DTO, a encja zostaje "brudna".

O to właśnie chodziło mi w poprzednim moim komentarzu - wykorzystanie techniki OO do problemów, do których nie służy.

Pytanie: czy jest to dywagacja akademicka - pseudonaukowy turobełkot...? W mojej karierze naukowej nie spotkałem teoretyków, którzy tak na prawdę czują OO. Pamiętajmy, że OO powstało w laboratoriach komercyjnej firmy w odpowiedzi na PRAKTYCZNE bolączki z utrzymaniem systemów.
Zresztą... OO w wydaniu C++, Javy czy C# (tak zwanych języków wąsatych) jest mocno strywializowane. Jeszcze w latach 80 OO było dużo bardziej zaawansowane, np w Smalltalku.

milus pisze...

W sprawie tych interfejsów chyba się nie porozumiemy.
Ja poprostu widze to w taki sposób, że zależności między warstwami są oparte na interfejsach. I to wszystko ...
A to, że te interfejsy mogą byc implementowane przez obiekty domenowe to zupełnie inne sprawa.
Jest to szczegół implementacyjny: a dla mnie bardzo kuszący szczegół, który pozwala dodatkowo poradzic sobie z lazyloadingiem.
Warto pamiętac że jak XXX mi zniknie z obiektu domenowego to mogę zmienic aby obiekt domenowy nie implementowal juz tego interfejsu, i stworzyc sobie DTO, które będzie spełniało kontrakt.

A w sprawie tych "select new" to nie googlowałem, ale z moich prób wynika, że HQL spłaszczy tą kolekcje: tzn dostane zbiór userDTO który każdy będzie miał pojedyńczy adres, zamiast userDTO który będzie miał zbiór odpowiadających mu adresów.
Po prostu będą duplikaty, które trzeba będzie jakoś sobie obsłużyc...
Oczywiście zgadzam się z Tobą,że może nie byc sensu dostawac usera w raz z odpowidającym mu encjami Address, ale wtedy robienie potworków w stylu:
"select new(u.id,u.name,a.city,a.street)
from User u join u.addresses a
" jest tak naprawdę strzelaniem sobie w stope.
Nie tylko konstruktor będzie baaaardzo długi, ale cały model i tak będzie spłaszczony...
oczywiście można z tym zyc ale jest to bardzo bardzo niewygodne.

Sławek Sobótka pisze...

ok, zostawmy te interfejsy:)
Jestem w stanie zaakceptować wyjaśnienie, że to pewien styl/konwencja architektoniczny i się go trzymamy.

Chcę jeszcze wyjaśnić jedną kwestię, bo być może ona blokuje w moim mózgu jakąś furtkę:
Napisałeś, że interfejsy pomagają w radzeniu sobie z Lazyloadingiem...
O ile pamiętam w innym poście pisałeś, że masz metody DAO/"Repo", które są przeciążone zwracanym typem. Czy o to chodzi?

Odnośnie jeszcze ostatniej uwagi a'propos spłaszczania modelu przez DTO. Być może po prostu niedokładnie to sformułowałeś, ale na wszelki wypadek uściślę: Cały koncept z Command-query Separation polega na tym, że model domenowy to jedna rzecz a DTO to tak zwana inksza inkszość.
Model pracuje nad logiką biznesową. A DTO to tylko jakaś głupia perspektywa - widok na ten model. Widok odpowiedni dla danego ekranu/klineta - ogólnie dla konkretnego Use Case.
DTO to, że tak powiem śmietnik, różne klasy DTO są tworzone często i gęsto, żyją tak długo jak żyje dany ficzer w systemie. Model za to jest czymś corowym.

Sławek Sobótka pisze...

Zapomniałem odnieść się do argumentu wygody, który właśnie chyba jest kluczowy w całej dyskusji.

Wygoda może być różnie rozumiana - w zależności od natury danego systemu (rozległość, poziom komplikacji domeny, poziom komplikacji architektury = moduły, warstwy itd).

Z jednej strony tą wygodę możemy rozumieć "lokalnie": programuję sobie dany ficzer i wygodnie jest mi pobrać encję, podpiąć ją pod formularz i cześć pieśni. Zgadzam się, że robienie tego przez DTO to nic innego jak nakładanie gaci przez głowę. Ba, nawet interfejsy bym sobie darował.

Ale będziemy mieli do czynienia z zupełnie inną klasą problemów gdy system podzielimy na moduły i założymy 15 lat utrzymania systemu w trakcie którego nastąpi 5 zmian technologii prezentacji i nieustanne podmiany modułów.

Wówczas na prawdę wygodniej jest posadzić praktykanta, który radośnie będzie pisał DTO. Wygodniej będzie kiedyś zmieniać implementację modułu nie burząc innych modułów i podsystemów.

Jak zwykle dochodzimy do wniosku, że trzeba dobrać właściwy młotek do problemu. I wcale nie jest tak, ze dobór obowiązuje dla całego systemu. Jak najbardziej jestem za dobieraniem architektury pod konkretny problem i nie widzę przeciwwskazań aby mieć w systemie 3-5 różnych stylów architektonicznych.

Miłego popołudnia życzę.

milus pisze...

Witam

Jestem ciekaw czy ktoś oprócz nas 2 to jeszcze czyta...
Temat jest ciekawy i nadaje sie na dłuższą dyskusje face2face w jakimś sympatycznym pubie...

Wracając do tematu:
Interfejsy:
DAO zwraca zawsze obiekty domenowe, warstwa wyższa (np ta która realizuje query) zwraca interfejsy. Skoro dany interfejs jest zaimplementowany przez klase domenową to można w runtime zwrócic faktyczny obiekt domenowy (lub tez DTO jakby ktos chcial).
Może troszke kodu (model bez setterów/getterow)

public interface IUser{
Long getId();
String getName()
}

public class User implements IUser
private Long id,
private String name,
private Set<Addresses> a;
}
Teraz jeśli nasza warstwa GUI widzi tylko IUser - i nie można się dopchac do addresów, które mogą byc niezainicjalizowane.
W bardziej skomplikowanym przypadku gdy chcemy zwrócic user i jego addresy to wyglądałoby to tak:
public interface IUserWithAddress extends IUser{
Set<? extends IAddress&lgt; getAddresses();
}
Teraz ważne: gdyby interfejs IAddress byłby zaimplementowany przez Address to IUserWithAddress może zostac zaimplementowany przez User.
Co dodatkowo jest ważne: jeśli wychodzimy od modelu (co pod wpływem naszej wymiany poglądów gadzam się, że nie jest to zgodne z pure oo) do tworzenia interfejsów to większośc takich zabiegów można "wyklikac" z IDE.
Dodatkowo można bardzo szybko i łatwo pisac testy sprawdzajace czy cały graf opisany przez interfejs jest zainicjalizowany


DTO: zgadza się że DTO to taki snapshot potrzebnych danych per use case. I to wszysko. Zgadzam się, że DTO do tego służy i koniec. Nie kwestionuje też CQS, bo w pewnym sensie sam z tego nieświadomie korzystam (a już teraz świadomie)
Niestety w jakimś tam procencie (niestety wysokim) snapshot danych który jest reprezentowany w postaci DTO bardzo często odpowiada temu co mamy w modelu.
Oczywiście nie jest problemem stworzyc DTO, ale problemem jest je wypelniac.
A pisanie testów które spradzają czy w odpowiedni sposób przepisuje prpoertiesy z 1 obiektu do 2 nie należą do najbardziej frapujących zajęc.

Niestety jak dla mnie zabieg, który podałeś w woim poscie - korzystac z "select new ()" , jest trochę mało użyteczny. Pomijam przypadek zapytań raportowych, czy też zaawansowanych wyszukiwań, ale w jakis sposób korzystając z "select new()" dostac usera i jednocześnie jego liste adresów?
Chyba takie wymagane nie jest jakieś rzadkie/unikalne

Z moich prób (mam nadzieje, że ktoś mi wytknie, że się nie znam)
wynika z tego że przy takich konstrukcjach wynik HQL odpowiada w dużej mierze temu co mamy w czystym SQL. Czyli jak dany user ma 5 adresów to w wyniku dostaniemy tego usera powtórzonego 5 razy, bo każda z instancji będzie posiadała unikalny adres.
Czy jest to źle? Hmm, no jest dobrze, ale za to jak jest niewygodnie pracowac z taką plaską strukturą po stonie widoku.
oczywiście możemy sobie napisac warstwe (warstewke) która zaagreguje coś takiego, ale chyba lepiej nie narażac sie na smiesznosc

A co do wygody: zgadza się, że dla każdego znaczy to pojęcie coś innego. Jak dla mnie DTO to przede wszystki podstawa jak nasz system jest rozproszony: warstwa GUI/WEB dobiera się zdalnie do warstwy aplikacji.

Z pracktycznego punktu widzenia dobieranie wielu styli architektonicznych w ramach 1 systemu ma jedną zasadniczą wade: mniej wdrożeni/zaawansowani developerzy mają wybór - ale lepiej im tego zaoszczędzic :)

Sławek Sobótka pisze...

Miesięcznie bloga czyta ok 700 osób więc jest szansa, że jakiś procent z nich zajrzy do komentarzy;)
Szczególnie, że jeżeli komuś już się chce czytać bloga technicznego (w jakimś stopniu) to znaczy, że raczej interesuje się tym co robi na co dzień trochę bardziej niż odrobienie pańszczyzny.

Odnośnie wielu stylów architektonicznych w 1 systemie i zagubienia junior_developerów: tak jest to pewien problem.

Ale z własnego doświadczenia gdy kiedyś sam zbytnio uogólniałem zrozumiałem, że o wile gorzej jest opracowanie zbyt zaawansowanej architektury z powodu 10% skomplikowanych kejsów i męczenie się z nią w całym systemie.

Analogicznie tak samo złe jest opracowanie arch. zbyt prostej gdy system w niemałym stopniu prosty nie jest.

Jednak nie obejdzie się wówczas bez kontroli kodu, no i dobrą inwestycją będzie choćby pobieżne zaprojektowanie rozwiązania (w danym stylu) przed rzuceniem na nie junior_develoepra.

Sztuką jest jak pisałem dobranie młotka do problemu. Albo w ogóle zdecydowanie czy potrzebujemy młotka czy może koparki;)

Odnośnie przepisywania Encja-DTO to jednak zwykle jest, że jeżeli system jest na tyle zaawansowany, że wprowadzamy DTO to równocześnie nie będziemy w tych przypadkach używać ORM. SQL wyciągający przekrojowe (odpowiedź na Query) dane i pakowanie resultet-DTO.

Ale jeżeli jednak idziemy w tą stronę, czyli przepakowanie to polecam ciekawy sposób na budowanie DTO z encji:
http://art-of-software.blogspot.com/2008/12/up-ddd-in-action-hermetyczne-agregaty.html
Budowniczego można sobie parametryzować i nie będzie to już "robota dla szympansa"

Ale może zostawmy już te DTO...:)

Chciałbym jeszcze odnieść się do przykładowego kodu, który napisałeś:
DAO zwraca obiekty domenowe.
Warstwa wyższa (nie wiem jak podchodzisz do sprawy: aplikacji, fasady, jakieś servisy) zwraca interfejsy.
Jeżeli jakiś getter istnieje w interfejsie to zakładamy, że implementująca go encja (ale nie koniecznie encja) ma chciwie podciągnięty dany obiekt.

Jedna rzecz tutaj jest niespójna:
Gdzie leży wiedza wiedzy o tym co podciągnąć - lub inaczej: który byt za to odpowiada?

Wygląda na to, że 2 warstwy zajmują się tym aspektem.

To w DAO są kwerendy więc to DAO decyduje o tym co chciwie podciągnąć (gdzie dokleić JOIN FETCH w HQL albo gdzie dodać setFetchMode w Criteria API).

Ale DAO zwraca encje więc skąd ma wiedzieć w danym wywołaniu metody szukającej co zrobić?

Ta wiedza jest w metodzie z warstwy wyższej. Czyli warstwa wyższa woła DAO z jakimiś parametrami, typu KryteriaDociągania.

Przy jakieś zmianie musimy zmieniać 2 warstwy.

Zaznaczam, że nie twierdzę jakoby ten fakt dyskwalifikował ten styl. Nie jest to jeszcze jakaś tragedia, po prostu warto o tym pamiętać:)


A przy najbliższej okazji chętnie skorzystam z zaproszenia do sympatycznego pubu:)

Irek Matysiewicz pisze...

A ja potrafię osiągnąć ponad 700 wejść na bloga w 2 dni:
http://www.dzone.com/links/make_reflection_as_fast_as_direct_calls.html?ref=ps

Oczywiście nie chodzi tu o ściganie się. Ot tak chciałem się pochwalić. :-)

Sławek Chmiel pisze...

Hmmm... wydaje mi się, że ilość wejść nie jest aż tak istotna.

W blogu takim jak ten nie chodzi żeby ktoś kliknął i pooglądał sobie obrazki. Większość ludzi który interesują się pewnym zagadnieniem szuka tutaj wiedzy i to dość konkretnej.

To że ktoś kliknie na jeden z 1000 linków znajdujących się na dzone nie znaczy od razu że przeczyta dany post i będzie to dla niego wartościowe...

Wydaje mi się, że tutaj ważniejsze jest po prostu dzielenie się widzą a nie linkami ;)

I co ważniejsze sam post nie jest najważniejsze, według mnie jest bardziej forma sygnał do mózgu zainteresowanego żeby zastanowić się nad danym tematem i rozwijać go wspólnie. I tak np. jest w przypadku tego posta gdzie najwięcej informacji można znaleźć w samych już komentarzach.

Irek Matysiewicz pisze...

@SławekCh
Dokładnie zmierzyć jakości artykułu czy bloga się nie da. Miary takie jak ilość wejść, średni czas spędzony na stronie, ilość powrotów, ilość komentarzy to tylko pewne wskazówki dla autora. Ale lepsze to niż nic.

milus pisze...

Ostatnie zdanie w sprawie DTO:
nie do końca mogę się zgodzic z tym że jeżeli system skomplikowany to wprowadzamy DTO. Jak dla mnie DTO tak na prawdę jest bardzo użyteczne/niezastąpione gdy system jest rozproszony lub gdy mamy zapytania raportowe/przekrojowe.
Ale zostawmy ten temat.

A co do tego braku odpowiedzialności:
warstwą odpowiedzialną jest wartstwa nad DAO, nazwijmy ją roboczo warstwą "Query". Warstwa ta zwraca interfejsy i wie jaki fragment grafu jest do wyciagniecia...
Weryfikacją tego kontraktu są testy integracyjne.

Jesli chodzi o to że warstwa Query wywołuje DAO a pisanie sensownego kontraktu na poziomie DAO jest niemożliwe - zgadzam się w 100 % z twoim postem: http://art-of-software.blogspot.com/2009/04/dao-sprawa-lazy-loading.html,
dlatego posiłkuję się bardzo konstrukcją Hibernate: Hibernate.initialize().
Oczywiście mogą się odezwac głosy że wyciągam brudy(szczególy związane ze sposobem dostępu do BD) ponad warstwe DAO, ale można ten fakt ładnie przykryc jakimś interfejsem :)

Niestety nie jest to silverBullet:
gdy będziemy chcieli zwracac liste userów wraz z odpowiadającymi im adresami to
iteracja po liscie userów i doczytanie adresów za pomocą Hibernate.initialize() niestety daje n+1; chyba, że używamy BATCH lub SubSelect do ładowania powiązanej kolekcji.
Ilośc zapytan nam urośnie ale przynajmniej objedzie się bez LazyInitializationExceptio po stronie WEB.

Dodatkowo, staram się z "niedoskonałoscią kontraktu DAO" walczyc poprzez pisanie testów integracyjnych dla metod DAO. Nie jest to może super wygodne ale z mojego doświadczenia naprawdę działa.

Przemek pisze...

Może temat jest już stary ale wydaje mi się ze problem "klepania" obiektów DTO oraz specjalnych assemblerów i konwerterów wciąż ma miejsce w typowych aplikacjach. Czy rozwiązanie proponowane przez SEAM jest wstanie wyeliminować ten problem na duższą metę czy jednak fajnie sie to sprawdza tylko przy małych i prostych projektach CRUD. Co z problemem n + 1 select w seam ?

Unknown pisze...

Czyli jezeli mam GUI ktore bazuje na koncepcji person.getAddress lub person.getEmployee i wyciaga polowe bazy bez lazy loadingu to co wtedy? jak to mam zoptymalizowac?

Sławek Sobótka pisze...

@Przemek:
Seam bardzo sprzyja pojawianiu się n+1 SP.
Wszystko dzięki "ułatwieniom" w postaci Open Session in View.

Przykładowo gdy masz tabelkę w JSF i podłączysz do niej listę osób. Dalej w jednej kolumnie tej tabelki wyświetlasz listę adresów danej osoby z danego wiersza.

Gdyby nie OSiV to dostalibyśmy wyjątek LazyInit, który to zmusiłby nas do myślenia o tym jak rozumnie pobrać potrzebne dla widoku dane.

Ale mamy OSiV, który pozwala bezmyślnie stukać kolejne ekrany, co kończy się problemami z wydajnością.

W dalszej kolejności zaczynamy skalować system (bo muli) oraz wprowadzać rozproszony cache;)))

@Wladyslaw
Nie do końca rozumiem pytanie, ale chyba pytasz o to, czy ja mam np. edytor osoby, to czy wówczas warto unikać Lazy Loadingu.

Nie.

W takim przypadku nie. Jeżeli jakiś ekran operuje na jednej encji (plus ew. encje zagregowane) to nie występuje n+1 SP. Ot po prostu mamy kilka zapytań do bazy (np. o usera, o jego numery tel. itp). Tak więc kila zapytań to nie tragedia na salę n+1.

Mało tego... zwykle lepszym podejściem pod względem wydajności może być wysłanie kilku zapytań o potrzebne obiekty, niż jednego zapytania z paroma joinami.

Tak więc resumując: n+1 SP występuje (i boli) w przypadku list. Natomiast w przypadku małych grafów kilku obiektów LL bardzo pomaga zwiększając produktywność programisty.

milus pisze...

@Władysław
Jak nie muli to po co opytmalizować?
A jeśli muli to trzeba sprawdzić czemu. Jeśli nie masz lazy loading to możepo prostu za dużo wyciągasz z BD jednym zapytaniem - problem może leżeć nie tylko w "trduności" zapytania, ale może dotyczyć filtrowaniem po stronie Hibernate (w spec przypadkach Hibernate filtruje w pamięci) czy też ilością zajętej pamięci która utrzymyuję to co przed chwilą wyciągnąłeś.

@Sławek
Na czym bazujesz swoją opinie że "lepszym podejściem pod względem wydajności może być wysłanie kilku zapytań o potrzebne obiekty". Staram się w takich przypadkach zmierzyć oba podejścia, ale z mojego doświadczenia z Oracle, DB2, MySQL w 90 % jest właśnie odwrotnie: lepiej zrobić joina niż wydać 2 zapytanie. Oczywiście nie dotyczy to przypadków w których to otrzymamy jakiś wielki iloczyn kartezjąński

Sławek Sobótka pisze...

@milus
O to właśnie mi chodzi - o kartezjan. Ale to i tak zawsze trzeba by zmierzyć i porównać.
Często jest też tak , że graf obiektów wymaga nie 2 i nie 3 ale więcej joinów.
To zależy, ale wówczas raczej lepsze będzie kilka zapytań.

Ale to co napisałeś jest zgodne z jedną z głównych zasad optymalizacji - aby optymalizować trzeba mierzyć.

@Wladyslaw
Doczytałem jeszcze, że piszesz o wyciąganiu przysłowiowej "połowy bazy".
Generalnie mapowanie obiektów na tabele w taki sposób, że w modelu obiektowym mogę pobrać ogromne (setki+) listy lub bardzo rozległe grafy obiektów jest błędne.

Ja wiem, że takie mapowanie fundują nam automaty. Ale takie mapowanie po pierwsze narusza wszelkie zasady OOD a po drugie zwykle jest bezsensowne z punkty widzenia GUI albo rozumowania biznesowego.

W takim wielkim grafie obiektów trzeba sobie wyróżnić osobne konteksty i pobierać je z osobnych serwisów/dao/repo.

Przykładowo w DDD mamy wskazówki jak modelować Agregaty w kontekście Bounded Context.