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.

7 komentarzy:

Rafał Jamróz pisze...

Bardzo ciekawe uwagi. Nie zebym sie czepial JPA, ale "leaky abstraction" dobrze pasuje do tej specyfikacji :).

Wg JPA takie cos jak lazy-initialization w ogole nie istnieje! Specyfikacja nie mowi np. ze pobranie zagregowanych zaleznosci moze sie nie udac (LazyInitializationException nie jest wyjatkiem JPA).

Ustawienie fatch=LAZY, to tylko wskazowka dla konkretnej implementacji persistence providera, ale moze nie byc wziete pod uwage.Ot taki opcjonalny ficzer Tylko po co w specyfikacji pisac wymagania ktore nie sa obowiazkowe?? Przeciez i tak kazdy moze sobie dodac do swojej implementacji co mu sie zamarzy :).

Sławek Sobótka pisze...

Brak specyfikacji reagowania na taki szczegół jak niemożliwość "podciągnięcia" zagregowanych obiektów jest wg mnie błędem kardynalnym.

Że niby gdy użyję sobie jakiegoś JpaMadeInChina, który zamiast walić swojego SPECYFICZNEGO (niestety) wyjątku zwróci mi radośnie null??? hehe

Unknown pisze...

Z tym nullem, to niestety nie trzeba szukać daleko w Chinach. Jest sobie np. taki OpenJPA, który tak właśnie się zachowuje.

milus pisze...

Jeśli chodzi o sprawe związaną z tym, że ciężko jest wyspecyfikowac jaki fragment grafu obiektów ma byc zwracany to można sobie z tym poradzic poprzez zwracanie interfejsów, które to będą implementowane przez nasze klasy modelu. Nie jest to moje autorskie rozwiązanie ale ściągnięte z "Pojo in Action".
W java 5 dzięki covariant return type robi się to na prawdę bardzo łatwo i przyjemnie.

Oczywiście często w większych projektach dochodzi do mnożenia się interfejsów, ale uważam że jest to i tak czystsze i bardziej produktywne niż
- łudzenie się że w kodzie w którym sesja została już zamknięta nie odwołamy się do zainicjalizowanych powiązań
- tworzenie DTO

Sławek Sobótka pisze...

Jest to jakieś rozwiązanie...
Wszystko zależy od skali systemu. Osobiście za najbardziej eleganckie uważam podejście zwane Greg Young's DDD gdzie:
- encja (w rozumieniu DDD czyli nie anemiczna lecz posiadająca odpowiedzialność biznesową) nie wychodzi ponad warstwę aplikacji
- wówczas nie będzie nigdy problemu z zamkniętą sesją
- poza warstwę aplikacji wychodzą DTO (ale nie przepakowane z encji, ponieważ to bez sensu)

Wówczas:
- nie mamy niezręczności opisanych w poście
- mamy trochę więcej roboty, więc ma to sens tylko w perspektywicznych projektach.

milus pisze...

Musze w takim razie sprawdzic na czym polega Greg Young's DDD...
Bo nie do konca rozumiem na czym polegają: "DTO (ale nie przepakowane z encji, ponieważ to bez sensu)"
Budowanie non-anemic-domain dla CRUD jest troche jak wyciaganie armaty na słonia, ale nawet w dużym, skomplikowanym projekcie zawsze znajdzie się funkcjonalnośc polegająca na wyciągnięciu czegoś z bazy danych tak jak jest (np lista userów).
Wtedy ciężko się nie pokusic o to aby obiekty domenowe nie wyciagnac na web zamiast tworzenia DTO i przepisywania 1:1 potrzebnych w tym case propertiesów.

Sławek Sobótka pisze...

Racja, nie ma nic gorszego niż przekombinowanie:)

Jeżeli mamy do zrobienia mały systemik (powiedzmy 3 miesiące i po sprawie) to uciekanie od Encji będzie niczym przysłowiowe nakładanie gaci przez głowę.

Kolejne aspekty oprócz powagi aplikacji to: wydajność i bezpieczeństwo (szczególnie w kontekście grubych klientów).

A co do pobierania dto wprost z bazy to w najnowszym poście o tym wspomniałem.