Pokazywanie postów oznaczonych etykietą JPA. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą JPA. Pokaż wszystkie posty

piątek, 19 grudnia 2014

Video z prezentacji na JDD 2014: JPA i DDD

Mapowanie relacyjnono-obiektowe prawdziwych obiektów.

Slajdy: http://prezi.com/fsvd1xnvecff/mapowanie-relacyjno-obiektowe-prawdziwych-obiektow/

Główne tezy:
  • nie używaj lazy loadingu
  • uważaj na typ kolekcji
  • rozważ 3 sposoby optimistic locking
  • wzorzec Repository ma więcej sensu się wydaje
  • CQRS na ratunek

...a tu więcej do poczytania: http://bottega.com.pl/artykuly-i-prezentacje






Jak wciągnąć eksperta domenowego w wir modelowania


Główne tezy:
  • Model jest narzędziem komunikacji
  • Przydawki są ważne:)
  • Można myśleć funkcyjnie i programować obiektowo
  • Proces jest pochodną domeny
  • User story jest słabe - potrzebujesz domain story
  • Refaktoryzacja bez zrozumienia domeny jest zgubna






sobota, 8 marca 2014

Artykuł: Zarządzanie transakcjami w systemach klasy enterprise

Kilka istotnych szczegółów odnośnie transakcji, które mogą uchronić Cię przed katastrofą znajdziesz w artykule Zarządzanie transakcjami w systemach klasy enterprise, który opublikowałem w lutowym numerze programistamag.

Do pobrania tutaj:  http://bottega.com.pl/artykuly-i-prezentacje#tx.

Niepoczątkujący czytelnicy mogą śmiało pominąć wstęp, następnie nakreślamy pokrótce generalne strategie zarządzania transakcjami aby wreszcie zająć się najprostszą z nich:) Mimo, że najprostsza to wciąż czycha tam na nas wiele pułapek, szczególnie w połączeniu z JPA.

Uzupełnieniem lektury jest artykuł Mapowanie relacyjno-obiektowe prawdziwych obiektów http://bottega.com.pl/pdf/materialy/receptury/orm.pdf. Temat rozwinę na prezentacji pod tym samym tutłem podczas zbliżającej się konferencji 4Developers - abstrakt tutaj.

sobota, 29 czerwca 2013

Mapowanie relacyjno-obiektowe prawdziwych obiektów – rzecz o DDD i JPA

Czy wiesz, że:
  • Lazy Loading nie ma sensu
  • Mapowanie @OneToMany z wykorzystaniem tabeli linkującej (domyślne zachowanie hibernate) nie ma sensu
  • Blokowanie Optymistyczne oparte jedynie na @Version nie ma sensu
...jeżeli stosujesz zasady Object Orinted i JPA (lub inny maper relacyjno-obiektowy).


Jeżeli powyższe tezy targnęły Twoimi emocjami, to zapraszam do lektury najnowszego artykułu: Mapowanie relacyjno-obiektowe prawdziwych obiektów – rzecz o DDD i JPA (do pobranie całkowicie free), który ukazał się w najnowszym numerze programistamag.pl (wakacyjny numer o podwójnej objętości dostępny w Empikach lub w formie elektrycznej).

Zapraszam do śledzenia całej serii Laboratorium Bottega - Receptury projektowe – niezbędnik początkującego architekta.

piątek, 2 marca 2012

Racjonalne wykorzystanie JPA

W poprzednim poście opisałem klasyczny problem wydajnościowy N+1 Select Problem występujący podczas korzystania z Java Persistence API, wraz z kilkoma podejściami do zabezpieczenia się przed jego powstawaniem.

W komentarzach pojawił się pewien wątek, który chciałbym teraz rozwinąć...

Zastanowimy się nad racjonalnym wykorzystaniem narzędzia jakim jest maper relacyjno-obiektowy. Jak to zwykle z racjonalnym myśleniem bywa - niesie ono ze sobą zwykle dodatkowe skutki uboczne w postaci nieoczekiwanych korzyści. W tym wypadku będzie to dodatkowa poprawa wydajności.

Co? Po co? Dlaczego tak?

W jakim celu mapujemy świat relacyjny na obiektowy?
Być może po to aby:
  • Pobierać w wygodny sposób obiekty biznesowe - wraz z wygodnymi mechanizmami typu Lazy Loading
  • Wykonywać na nich operacje biznesowe zmieniające ich stan - możemy tutaj tworzyć zarówno anemiczne encje modyfikowane przez serwisy jak również projektować prawdziwe obiekty modelujące reguły i niezmienniki biznesowe (styl Domain Driven Design)
  • Utrwalać stan obiektów biznesowych - stan, który zmienił się w poprzednim kroku (korzystając z wygodnych mechanizmów wykrywania "brudzenia" i mechanizmu kaskadowego zapisu całych grafów obiektów)
Jeżeli używasz JPA do tych klas problemów, to używasz odpowiedniego młotka do odpowiedniej klasy problemu. Czyli pobieram obiekt (JEDEN, no dwa, góra cy;) biznesowy, zmieniam jego stan, zapisuję go.

Pisząc zmieniam stan nie mam na myśli "edytuję podpinając pod formularz". Mam na myśli logikę aplikacji (modelującą Use Case/User Story), która modyfikuje mój obiekt biznesowy (uruchamiając jego metody biznesowe lub settery jeżeli jest on anemiczny). Przykład gdzie Order i Invoice to obiekty persystentne:

 public void approveOrder(Long orderId) {
        Order order = orderRepository.load(orderId);

        //sample: Specification Design Pattern
        Specification<Order> orderSpecification = generateSpecification(systemUser);
        if (!orderSpecification.isSatisfiedBy(order))
            throw new OrderOperationException("Order does not meet specification", order.getEntityId());

        // sample: call Domain Logic
        order.submit();
        // sample: call Domain Service (Bookkeeper)
        Invoice invoice = invoicingService.issuance(order, generateTaxPolicy(systemUser));

        invoiceRepository.save(invoice);
        orderRepository.save(order);
    }

Link do kodu.


Zawsze?

Jeżeli natomiast chcę wyświetlić na ekranie dane, np. dane przekrojowe, np w postaci tabelki (ludzie biznesu uwielbiają tabelki, najlepiej aby dało się przestawiać kolejność ich kolumn;) to narzędzie pod tytułem JPA nie jest odpowiednim młotkiem do tego problemu. W tym wypadku na każdym etapie postępuję nieracjonalnie:
  • Pobieram z mapera listę obiektów (zamapowanych na całe tabelki w bazie) gdy potrzebuję na ekranie jedynie kilku kolumn z każdej tabelki (dla bazy nie robi to różnicy, ale gdy maszyna serwująca bazę lub klient je zdalna to wówczas zaznamy odczuwać skutki tej decyzji)
  • Mam możliwość korzystania z mechanizmu Lazy Loadingu, który nie ma sensu dla operacji typu "pobierz dane do wyświetlenia"
  • Silnik mapera wykonuje niepotrzebne operacje związane z LL i wykrywaniem "brudzenia" - przecież nie będę modyfikował tych obiektów, chcę jedynie coś wyświetlić
  • Zdradzam model biznesowy warstwie prezentacji. Być może w prostych aplikacjach z prezentacją w technologii webowej (ta sama maszyna pobiera i prezentuje dane) nie jest to problem - dodatkowo zyskujemy produktywność w pracy. Ale jeżeli klienty (nie klienci) są zdalne (np Android)? Zdradzanie modelu domenowego wiąże się z drastycznym spadkiem bezpieczeństwa (wsteczna inżynieria) oraz z koniecznością koordynacji prac zespołów pracujących nad "klientem" i "serwerem", że o zapewnianiu kompatybilności starszych wersji klientów nie wspomnę. Niby banały, jednak w niektórzy ewangelizatorzy EE zachęcają do zwracania encji ponad warstwę serwisów (sic!)


Klasa problemu <=> Klasa rozwiązania

W każdym systemie mamy wyraźnie rozróżnienie na odczyt danych i modyfikację danych. Pisałem już jakiś czas temu o paradygmacie Command-query Seapration, z którego wyłoniła się architektura Command-query Responsibility Segregation.

Nie chcę się powtarzać, zatem w materiałach bloga (link na górze po prawej) znajdziecie prezentację na ten temat, polecam też artykuł samego mistrza: Martina Fowlera.

Separacja

Tak więc donosząc się do pytania Andrzeja z poprzedniego posta: Jak najbardziej konstrukcja SELECY NEW MyDTO(encja.pole1, ecnja.pole2) FROM Encja encja jest na miejscu. W przypadku gdy chodzi o zwrócenie danych do prezentacji (modelowanych jako DTO) a nie pobranie obiektów biznesowych do wykonania na nich operacji biznesowych.

Separację możemy poprowadzić jeszcze "głębiej" i dokonać projekcji modelu domenowego, który utrzymujemy w III postaci normalnej do postaci płaskiej, odpowiedniej do odczytu. W tym celu możemy zastosować Widoki Zmaterializowane lub np. odświeżać model Read przy pomocy zdarzeń domenowych.

Najmniej racjonalną rzeczą jaką możemy zrobić to pobierać encje JPA i przepakowywać je na DTO. Po co pobierać te dane (ryzykując N+1SP) skoro i tak musimy wykonać pracę (kodowanie, załączenie automatu) przepakowania?

Wydajność ++

Ale zostawmy zaawansowane architektury przygotowane do skalowania...

Jeżeli już decydujemy się na pobieranie poszczególnych kolumn z bazy, to dlaczego nie użyć czystego SQL zamiast HQL (konstrukcji SELECT NEW)? Przecież skoro wiem, że pewnych miejscach pobieram dane jedynie do odczytu, to być może warto zestawić osobną pulę połączeń z bazą - połączeń read-only. Być może baza, której używasz będzie wówczas działać nieco lepiej...:)

W takim wypadku warto użyć lekkiego mapera typu myBatis, którego użyję w celu mapowania Result Set na paczki danych (DTO) a nie na obiekty biznesowe służce do wykonywania operacji biznesowych!

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

Pracując z Hibernate (od 2003r, od wersji 2.0) zawsze, w każdym jednym projekcie - małym i dużym (1200 tabel) dochodziło do sytuacji przepisania pewnych zapytań na czysty SQL z powodu wydajności. MyBatis na prawdę działa:)

Przykłady architektury, która wyraźnie rozdziela operacje odczytu i zapisu: Domain Driven Design & Command-query Responsibility Segregation sample project - Java, Spring, JPA

wtorek, 28 lutego 2012

Automatyczne wykrywanie n+1 Select Problem w EJB (ale niekoniecznie)

N+1 Select Problem to... problem. Poważny problem:)
Temat jest stary jak Hibernate, ale w swej pracy wciąż spotykam zespoły, które nie zdają sobie z niego sprawy. Tak więc jeżeli nie wiesz czy masz n+1 SP to znaczy, że go masz.

Definicja problemu

Idea problemu jest prosta: załóżmy, że mamy encję User, która zawiera w sobie listę encji Address.
@Entity
public class User{
  @OneToMany
  @JoinColumn("user_id")
  List<Address> addresses;
}
Teraz jeżeli pobierzemy listę użytkowników a następnie iterując po tej liście dla każdego użytkownika zaczniemy przeglądać jego adresy, to wówczas możemy spodziewać się następującej interakcji z bazą danych:
1 x SELECT ... FROM Users - zwróci nam n użytkowników
N x SELECT ... FROM Address

Wykrywanie białkowe

Jeżeli w firmie gdzie pracujesz porządku w bazie pilnuje DB-Nazi, to możesz spodziewać się nagłej wizyty tego smutnego Pana...
Jeżeli nie masz takiego szczęścia (nie jest to sarkazm) to problem wykryjesz obserwując konsolę.
A jeżeli w swojej bazie developerskiej posiadasz 1 Adres (generalnie: operujesz na bardzo małych n) to zapewne problem uświadczysz dopiero na produkcji.

Wykrywanie automatyczne

Możemy stosunkowo łatwo zbadać ilość poleceń SQL wysyłanych do bazy danych. W tym celu posłużymy się klasą Statistics.
Aby zdiagnozować ilość operacji wykonywanych przez nasze komponenty biznesowe (serwisy, nie oszukujmy się;) możemy posłużyć się technikami AOP. Przykładowo w Springu mamy możliwość wpięcia Porady (Advice). Natomiast w EJB możemy skorzystać z Interoceptorów ("poor man's AOP" w wydaniu EE):
package pl.com.bottega.common.support.interceptors;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import org.hibernate.SessionFactory;
import org.hibernate.ejb.EntityManagerFactoryImpl;
import org.hibernate.event.EventListeners;
import org.hibernate.stat.Statistics;
import org.jboss.jpa.injection.InjectedEntityManagerFactory;

public class NPlusOneSelectProblemDetectingInterceptor {

 @PersistenceUnit
 private EntityManagerFactory entityManagerFactory; 
 
 
 @AroundInvoke
 public Object countStatements(InvocationContext invContext) throws Exception {
  InjectedEntityManagerFactory iemf = (InjectedEntityManagerFactory) entityManagerFactory;
  EntityManagerFactoryImpl hemf = (EntityManagerFactoryImpl) iemf.getDelegate(); 
  
  SessionFactory sessionFactory = hemf.getSessionFactory();
  Statistics statistics = sessionFactory.getStatistics();
  statistics.setStatisticsEnabled(true);

  long before = statistics.getPrepareStatementCount();
  
  Object result = invContext.proceed();

  long count = statistics.getPrepareStatementCount() - before;
  if (count > 30){
    String message = invContext.getTarget().getClass() + "->" + invContext.getMethod().getName() + " statements: " + count;
    //TODO wysłać maila do db-nazi 
  }
  return result;
 }
}

Nasz interoceptor jest stosunkowo prosty: sprawdza ilość Prepare Statement przed i po wywołaniu EJB. Jeżeli różnica przekracza badany pułap, wówczas "wiedz, że coś się dzieje".

Przechwycenie wszystkich EJB w pliku ejb-jar.xml:

 
  
   pl.com.bottega.common.support.interceptors.NPlusOneSelectProblemDetectingInterceptor
   
  
 
 
  
   *
    pl.com.bottega.common.support.interceptors.NPlusOneSelectProblemDetectingInterceptor
  
 

Problem wyższej warstwy

Drzewiej bywało tak, że transakcje opiewały jedynie warstwę logiki (nazwa umowna). W warstwie prezentacji Transakcje były niedostępne, przez co Entity Manager (Session w Hibernate) nie wspierał Lazy Loadingu. Dzięki temu programista dostawał wyjątek gdy LazyInitializationException gdy chciał "dociągać" dane z warstwy prezentacji. Było to dobre, ponieważ skłaniało do zastanowienia: co ja właściwie chcę zrobić, jakich danych potrzebuję...
Obecnie niemal standardem jest (anty) pattern Open Session in View, który daje możliwość naszym kontrolkom GUI na dociąganie danych. I tak na przykład tabelka renderująca listę użytkowników, może w jednej z kolumn renderować listę adresów. N+1 SP zapewniony...

Aby wykryć problem powodowany przez warstwę prezentacji należało by stworzyć odpowiedni Filtr na poziomie Servlet API.

Naprawa N+1 SP

Istnieje kilka szybkich "obejść" oraz jedno rzetelne, prawdziwe rozwiązanie:
  • Rozproszony cache Encji - ja podaję go w formie żartu, ale czasem widuje się to rozwiązanie. Być może jest ono uzasadnione, ale warto się zastanowić dlaczego go potrzebuję i jaką złożoność przypadkową ono wprowadza...
  • @BatchSize - redukuje problem, działa na ślepo konsumując pamięc, ale daje szybki efekt, można ustawić defaultowy, globlany w XML
  • @Fetch - wyspecyfikowanie w jaki sposób życzymy sobie pobierać chciwie/łapczywie/gorliwie (piękne tłumaczenia słowa EAGER) kolekcje. Uwaga: HQL ignoruje wszystko oprócz Subselect, Criteria API respektuje wszystko

Rozwiązanie rzetelne

Dedykowane zapytania "szyte na miarę" danego przypadku:
SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.addresses

Warto wiedzieć, że w tym wypadku setMaxResult działa w sposób niezdefiniowany oraz, że nie da się chciwie pobrać 2 Toreb (Bag). Bag to pojęcie w Hibernate, które oznacza kolekcję charakteryzującą się brakiem porządku (tak jak Set) ale zezwoleniem na duplikaty. Bag w Hibernate to na poziomie Javy List bez @IndexColumn... ehhh smaczki JPA...:P

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

W przypadku gdy zależy nam na wydajności nic nie zastąpi czystego SQLa tuningowanego przez starego, dobrego DB-Nazi, który zna się na swoim fachu...

środa, 14 grudnia 2011

DDD na platformie Java EE 6

Dzisiaj coś dla miłośników Enterprise Edition.

Nieustannie rozwijamy projekt ilustrujący implementację zaawansowanych technik Domain Driven Design i architektury Command-query Responsibility Segregation.
Dotychczasowa implementacja oparta na Springu doczekała się swego lustrzanego odbicia na platformie Java EE6
W projekcie znajdziecie między innymi kilka sztuczek w CDI oraz przykłady wykorzystania silnika zdarzeń w modelowaniu biznesowym (dowiecie się również dlaczego budowany Events<> jest nieco ułomny:).

Więcej szczegółów na blogu projektu.

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

W najbliższych planach aplikacja kliencka na Androidzie w architekturze Eventually Connected Client.

wtorek, 30 sierpnia 2011

Zaczyn DDD+CqRS

Jak pewnie zauważyliście, częstotliwość publikowania postów spadła ostatnio dosyć znacznie. A to za sprawą mocnego zaangażowania w nowy projekt - projekt ilustrujący techniki modelowania Domain Driven Design oraz wybrane podejścia do implementacji architektury Command-query Responsibility Segreation.

Projekt osiągnął poziom wersji Beta pierwszego Milestonea, tak więc już oficjalnie możemy go upublicznić: http://code.google.com/p/ddd-cqrs-sample/.



Zaczyn?
Nazwa ociekająca folklorem jest ilustracją idei: projekt to coś więcej niż Sample ale zdecydowanie nie jest to kolejny Framework - więcej na ten temat na wiki.
Gwoli wyjaśnienia: zaczyn to coś z czego powstaje chleb.

Odeszliśmy od formy frameworka przekonani przez samego Grega Younga (twórcę CqRS i propagatora DDD). Greg przekonał mnie podczas prywatnych rozmów, że framework dla CqRS delikatnie mówiąc nie ma sensu:)



Zawartość projektu odpowiada na pytania techniczne, które pojawiają się tak często, że uznaliśmy, że czas stworzyć sampla ilustrującego nasze podejście. Do tej pory istnieją dwa takie Sample napisane w Javie - my jednak chcieliśmy pójść nieco dalej. Tak więc projekt jest sumą doświadczeń jego twórców.

Co w nim znajdziecie:
- ilustracja wszystkich Builiding Blocków DDD (bez uproszczeń technicznych)
- ilustracja zaawansowany technik DDD (Bounded Context to nie są akademickie rozważania)
- ewolucyjne podejście do architektury warstwowej, różne poziomy separacji CqRS (na tym etapie nie wprowadzamy Event Sourcingu - wyjaśnienie powodów na wiki)
- pragmatyczne podejście do CqRS
- część bardzo specyficznej wiedzy wyniesioną przez nas z wiosennego szkolenia Grega Younga
- modelowanie czasu poprzez zdarzenia i dosyć nową technikę modelowania czasu: Sagi
- kilka eleganckich trików z wykorzystaniem Springa i Hibernate:)
- techniki testowania: jednostkowe, integracyjne i akceptacyjne
- połączenie JBehave (Behavior Driven Development) z Selenium
- już niedługo: nasz własny pomysł na uogólnienie historyjek tak by wspierały zarówno testowanie przez GUI jak i przez API



Zainteresowanych zapraszamy do:
- wiki - kilkadziesiąt stron A4 plus trzy wizualizacje w Prezi
- SVN - aktualnie ok 150 klas platformy zaczynu oraz przykładów DDD
- grupy dyskusyjnej
- bloga


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

Teksty techniczne z zakresu DDD i CqRS będę publikował na nowym blogu. Natomiast niniejszy będzie wciąż prowadzony jako miejsce na tematy pozostałe.

środa, 21 kwietnia 2010

Google Application Engine



Dziś będzie o rzetelności.

WYGLĄDA OBIECUJĄCO

Chmura Google - GAE ogólnie prezentuje się świetnie. Za darmo (do pewnych limitów) dostajemy platformę, która jest w stanie udźwignąć tysiące transakcji na sekundę oraz przechować dla nas terabajty danych w nierelacyjnej bazie BigTable. Do tego dostęp do wielu usług google takich jak bezpieczeństwo, składowanie obrazków, pobieranie zawartości stron poprzez ichni cache, potężny memcache itp.

Programować możemy sobie w Pythonie albo w Javie (okrojonej wersji, bez części klas standardowych i z zakazem korzystania z pewnych bibliotek).

Ogólne założenie jest takie: programiści (i konsultanci;) w większości nie potrafią konfigurować skalowalnych środowisk ani pisać skalowalnych aplikacji, dlatego dostarczmy im platformę, która zajmie się tym problemem w sposób transparentny. Czyli mamy podejście odwrotne do Java EE. Zajmij się problemem biznesowym a chmura zajmie się skalowaniem. Kierunek wydaje się słuszny.

Zatem zaczynamy zabawę...
Na dzień dobry krótka, oficjalna prezentacja z głównej strony jak to fajnie i szybko klepie się aplikacje i odpala je w kosmos: http://www.youtube.com/watch?v=P3GT4-m_6RQ
Trzeba przyznać, że plugin do Eclipse jest wzorowy: prosty w obsłudze (3 sugestywne buttony), działa oraz zapewnia wszystko:
- lokalny server emulujący chmurę i jej zasoby (bazę)
- narzędzie automatycznie "wzbogacające" bytecode klas encji na potrzeby ichniej "implementacji" JPA
- narzędzie wystrzeliwujące aplikację w kosmos
- prosty kreator, który generuje cały ten kosmodrom
(nie odważyłem się zmawenizować wygenerowanego projektu, bo to zwykle tydzień w plecy a na koniec może się okazać, że tracimy hotdeploy)



WTF?

Ale, ale... akcja filmu rozkręca się z czasem...
Jedna z największych firm programistycznych na ziemi pokazuje jak tworzyć kod w... Java Server Pages. Patrzcie uważnie i uczcie się: logika i dostęp do danych w... scriptletach (sic!).

Oczywiście duzi chłopcy wiedzą, ze scriptlety są depricated. A nóż widelec tą scenę zobaczy jakieś małe dziecko i będzie później tak programować jak dorośnie?!? Później znowu przeczytamy w gazecie o tragedii jaka rozegrała się w jakimś spokojnym projekcie. Gdzie byli rodzice?!?

Hmmm dziwne, dziwne... no ale nie czepiajmy się szczegółów, to przecież tylko taki przykład. Przecież zawsze możemy zaprojektować sobie eleganckie warstwy i całość oprzeć o sprawdzony framework.



BOLESNE OGRANICZENIA

Niestety tak jak wspomniałem na początku JVM ma ograniczenia i nie wszystko działa. Seam co prawda daje się uruchomić po zastosowaniu 15 speszyl haków, ale wszyscy twierdzą, że nie warto ryzykować. Na szczęście stary, dobry Spring oficjalnie działa. Wielu dzielnych blogerów publikuje hello worldy i tutoriale jak to poskładać. Wygląda na to, że jedni kopiują od drugich;)

Ba, nawet 2 książki o GAE preferują Springa i pokazują jak go skonfigurować. Hmmm ciekawe dlaczego omawiają tylko servlecik Spring MVC i dyplomatycznie nie poruszają bardziej zaawansowanych zagdanień...?

Niestety, jednak nie działa do końca!
Przykładowo gdy użyjemy technik Aspektowych i przeplatania w czasie uruchomienia to mamy dziwne błędy ClassNotFound. Lokalnie działa, a na chmurze już nie.
Co implikuje, że możemy zapomnieć o aspektowej, eleganckiej obsłudze bezpieczeństwa i transakcji.
Więc po co nam Spring? Tylko do wstrzykiwania? To bez sensu.



UWAGA NA PERSYSTENCJĘ

Nieważne, bez Springa można żyć, idźmy dalej... Nierelacyjna baza BigTable została przez Google przykryta interfejsem JDO albo JPA (do wyboru) więc czujemy się jak w domu.
Nie do końca... specyfikacja ma szereg ograniczeń wynikających z architektury rozwiązania BigTable - chodzi o rozpraszanie struktur danych na różne maszyny po stronie chmury.

Drobny szczególik wspomniany jedynie w dokumentacji jest taki, ze agregaty encji są grupowane w rodziny. Przykładowo jeżeli mamy encję Zamówienie, która ma w sobie listę ZamówionychProduktów to jest to rodzina. Co z tego wynika?

Encja może należeć jedynie do jednej rodziny.
Implikacja: Jeżeli mamy encje Zamówienie i Faktura oraz encję Kleint, który musi być podpięty do Zamówienia jak i Faktury to niestety nie ma takiej możliwości. Dziecko może mieć jednego rodzica. Oczywiście można to obejść i w Zamówieniu jak i Fakturze używać obiektów Kluczy do Klienta. Ale wówczas tracimy łatwość programowania i uzależniamy się od API Chmury.

Trzeba pamiętać, że podejście do modelowania danych w nierelacyjnej bazie wymaga innego "nastawienia" - taka natura rozwiązania. Jeżeli nierelacyjnie to może obiektowo? Jednak nie do końca - no to jak?

Osobiście preferuję takie podejście, że Zamówienie czy Faktura ma owszem klucz do Klienta ale ma również niepersystentne pole Klient, które jest ustawiane w niej osobnym zapytaniem (nie jest to duży koszt w bazie BigTable, która jest tak na prawdę wielowymiarową HashMapą). Tego typu szczegóły można hermetyzować z obiektach Repository, które zwracają dane "poskładane" tak aby było wygodnie. OK, jeden problem z głowy.


Transakcja może obejmować tylko operacje na jednej rodzinie.
Czyli jeżeli logika aplikacji chce zapisać zarówno rodzinkę Zamówienie jak i Faktura to będą to 2 osobne transakcje. A co jeżeli w tak zwanym międzyczasie zdarzy się coś przykrego? Ręczna rzeźba i sprzątanie:)
Poza tym, jak wspomniałem, aspektowe transakcje Springa nie działają, więc brudzimy kod biznesowy kodem API transakcji JPA.


Datanucleus - "implementacja" JPA
Oprócz tego, że jest okrojona to dodatkowo zawiera szereg błędów - nie działają nawet niektóre proste przykłady z tutoriali JPA. Na oficjalnej grupie wisi wiele pytań - bez odpowiedzi.



GWÓŹDŹ

Prezentacja
Z frameworków prezentacji testowałem JSF.
1.2 (z Seam) działa.
2.0 działa po zastosowaniu jednego prostego speszyl haka.

Działa w sensie, że się uruchamia. W 2.0 występują pewne problemy z wstrzykiwaniem ManagedBeanów. RichFaces ani IceFaces nie mają wersji dla chmury. Oficjalnie Primefaces częściowo działa. Znowu nie jest to rzetelna i informacja - AJAX ogólnie się sypie.

No i można zapomnieć o uruchamianiu metod z parametrem przy pomocy nowego silnika wyrażeń (el-api w wersji 2.2). Chmura odmawia współpracy z nowymi zdobyczami.

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

Biorąc pod uwagę powyższe ograniczenia:
- brak wygodnego frameworka (w tym wsparcia dla AOP)
- uboga funkcjonalność ORM wymagająca wielu ręcznych operacji
- mentalny model programowania sprzed o ok 10 lat
Mamy znaczny spadek produktywności.

Domyślamy się skąd te JSP, scriptlety i statyczne metody w filmie promocyjnym? Czyżby tylko to działało?
Szkoda tylko, że community i blogosfera zatrzymuje się na poziomie Hello World i nie weryfikuje nieco bardziej dogłębnie i rzetelnie.

Na zakończenie dodam, że zupełnie inne założenia przy budowie chmury przyjął Amazon. Dostajemy sprzęt i system operacyjny. Nikt nie narzuca nam dodatkowych bytów zbudowanych nad sprzętem i nie wchodzi nam z butami w nasz wypracowany latami i sprawdzony model programowania. I co najważniejsze - wszystko działa:)

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

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