"Patterns and Anti-Patterns in Hibernate" by Patrycja Węgrzynowicz - no prezentacja była dość przeciętna. Tematy, które zostały poruszone były całkiem ciekawe, ale było ich zdecydowanie za mało na 3 godziny. Na sam początek się trochę spóźniłem i trafiłem na omówienie rozwiązania problemu współbieżnego dostępu do danych w aplikacji CaveatEmptor z "Hibernate In Action". Problemem ma być implementacja funkcjonalności dodawania nowego bida. Algorytm jest taki, że przy dodawaniu nowego bida, sprawdzamy czy jest on większy od aktualnie najwyższej oferty. Implementacja jest następująca:
public someMethod(){
Bid currentMinBid = itemDao.getMinBid(itemId);
Bid currentMaxBid = itemDao.getMaxBid(itemId);
//lock for Upgrade
Item item = itemDao.getItemById(itemId, true);
Bid newBid = item.placeBid(userDao.findById(userId), newAmount,currentMaxBid, currentMinBid);
}
Patrycja pokazała, że może dojść do sytuacji, w której to będą składane jednocześnie 2 bidy (oba większe od aktualnie najwyższego bida): 1000 i 1002. W specyficznym scenariuszu może dojść do tego, że bid o wartości 1002 zostanie "wypchnięty" (każdy z wątków pobierze maxBid o wartośći 700, wątek bid-1002 zablokuję krotke, zmieni wartość na 1002 i zrobi commit, w tym czasie wątek bid-1000 będzie porównywać wartość 1000 z wartością 700, bo dla niego aktualnie najwyższy bid został wyliczony wcześniej, bid 1000 zostanie przyjęty i zostanie wykonany commit). W ten sposób bid o wartości 1000 zostanie dodany po bid wartości 1002. Ma to szczególnie znaczenie gdy mapujemy bidy jako listę a nie zbiór.
Proponowane rozwiązania to: użycie wersjonowania (rozumiem że chodzi o optimistic locking), ustawienie poziomu izolacji na REPEATABLE READ, ustawienie poziomu izolacji na SERIALIZABLE, zmiana kolejności instrukcji w kodzie. Pierwsze dwa rozwiązania mają być nieskuteczne. Jeśli chodzi o poziomy izolacji transakcji to sprawa jest tym bardziej ciekawa, gdyż niektóre bazy danych,np ORACLE nie implementują wszystkich poziomów, a nawet jeśli implementują, to implementacje mogą się od siebie różnić. Przykład rozwiązania, które nie działa, był oparty na MySql i pokazywał, że w momencie wydawania zapytania o item MySQL ma robić snapshot danych i reużywać go przy kolejnych zapytaniach. W wyniku takiego działania, końcowy rezultatem ma być dodanie 2 krotek do tabeli bidów, ale z tymi samymi wartościami w kolumnie definiującej pozycje na liście - przy następnym odczycie bidów Hibernate ma mieć problem z ich poprawnym załadowaniem. Chyba nie do końca trafia do mnie te wytłumaczenie. Szczególnie, że spodziewam się, że MySQL zrobi snapshot dopiero po uzyskaniu locka. W takim przypadku snapshot zrobiony jako drugi powinien widzieć zmiany zrobione w właśnie zakończonej transakcji. Nawet jeśli moje rozumowanie jest poprawne to chyba i tak nie da się w ten sposób rozwiązać oryginalnego problemu. Jeśli chodzi o SERIALIZABLE to rozwiązanie ma działać, ale chyba nie podejmę się dokładnej analizy. SERIALIZABLE zawsze automatycznie kojarzy mi się z problemem braku skalowalności i jest przyjmowane "z założenia" jako zbyt ciężkie.
Jeśli chodzi o wersjonowanie to dodanie mechanizmu versioning bez zmiany kodu nic się nie zmieni. Wynika to z tego, że druga transakcja uzyska lock po wykonaniu commit pierwszej, czyli już będzie miała zupdatowany licznik wersji. Użycie samego optimistic locking bez lockowania, też nie pomoże (wątek bid-1000 odczyta "stare" min/max bid , w tym momencie wykonany będzie commit transakcji wątku bid-1002, wątek bid-1000 odczyta item z wersją ustawioną przez wątek bid-1002, ale wykona porównanie ze "starymi" min/max bid). Rozwiązaniem jest zmiana kodu poprzez zmiane kolejności wywołań na:
public someMethod(){
//lock for Upgrade
Item item = itemDao.getItemById(itemId, true);
Bid currentMinBid = itemDao.getMinBid(itemId);
Bid currentMaxBid = itemDao.getMaxBid(itemId);
Bid newBid = item.placeBid(userDao.findById(userId), newAmount,currentMaxBid, currentMinBid);
}
W tym przypadku najpierw lockujemy a dopiero później wykonujemy kolejne operacje.Wydaję mi się, że taki kod byłby poprawny gdybyśmy zrezygnowali z lockowania krotki item, użyli jedynie optimistic locking, przy jednoczesnym updatowaniu item przy dodawaniu bida. Ten ostatni krok, ma jak najbardziej wartość biznesową - możemy trzymać tam datę ostatniego bidowania. W przypadku składania dwóch jednoczesnych bidów jedna z operacji zakończy się wyjątkiem OptimisticLockException. Wyjątek ten można (najlepiej za pomocą AOP) złapać a całą operację powtórzyć.
Patrycja wskazała, też alternatywne rozwiązania: obliczanie najwyższego bid programowo (wymaga załadowania całej listy bidów), redundentne przechowywanie najwyższego bid w item (denormalizacja BD, ale szybki dostęp przy ponoszeniu kosztów utrzymywania kopii informacji), wykorzystanie zaawansowanych możliwości związanych z mapowaniem. Ten ostatni pomysł wykorzystuję dość rzadko używane feature Hibernate i polega na tym, że do item dodajemy dodatkowe property wraz z mappingiem, dla którego jest wyspecyfikowane zapytanie(Embedded Query) definiujące to property. Zapytanie to ma mieć możliwość korzystania z order by, where, groupBy. Dodatkowo, dostęp do property możemy określić jako lazy/eager (w tym przypadku chyba najsensowniejsze jest określenie tego property jako set z fetch =FetchType.Lazy). Następnie zostały przedstawione 2 dość proste błędy w kodzie CaveatEmptor, aby w końcu przejść do większych problemów związanych z aplikacjami używającymi Hibernate a w ogólności dowolnego ORM. Chodzi tu o problem Anaemic Domain Model. Rozpoczęło się od próby zdefiniowania OOP i przedstawieniu podstawowych ( encapsulation/inheritance/polimorphism ) oraz zaawansowanych (SOLID) zasad świata OOP. W CaveatEmptor brak enkapsulacji jest bardzo widoczny co może spowodować wzrost ilości bugów (szczególnie w stylu czemu coś się dodało/usunęło z bazy) oraz problemy z maintainance. Rozwiązaniem ma być używania odpowiednich modyfikatorów dostępu, najlepiej mappowanie fields zmiast properties, defensive copying (w przypadku kolekcji skutkuje to pobraniem lazy mapped collection z BD), unmodifiableCollections (możliwy problem przedstawiony w przykładzie poniżej). Ważne jest dodatkowo zrozumienie w jaki sposób działa mechanizm dirty checking w Hibernate, szczególnie w stosunku do kolekcji entity i embedded objects - należy re-używać kolekcje które zostały stworzone/wypełnione przez Hibernate zamiast tworzyć nowe kolekcje (dirty checking ma wykorzystywać identity of collections w celu stwierdzenia czy kolekcja uległa zmianie). Patrycja pokazała dość podchwytliwy przykład:
public ListgetBids(){ return Collections.unmodifiableList(bids);}
public void setBids(ListnewBids){
bids.clear();
if(newBids != null){
bids.addAll(newBids);
}
}
W momencie gdy klient wywołałby:
bids = getBids();
setBids(bids);
to z bazy mogą zostać usunięte określone bidy. Problem jest w tym, że Collections.unmodifiableList() stworzy nam wrapper opakowujący podaną mu w parametrze listę, ale wszystkie zmiany na źródłowej liście są odzwierciedlane we wrapperze. W rezultacie do bids zostanie dodana pusta lista. Następne na tapetę trafiły problemy wydajnościowe Hibernate. Byłem bardzo zdziwiony, że nie zostały zbytnio poruszone problemy: n+1i cartesian product. Patrycja pokazała 2 przypadki:
- nadpisywanie listy:
A a = (A)session.get(A.class,2);
a.setBs(a.getBs());
W przypadku standardowego (a'la JavaBean) mapowania uruchomienie poniższego kodu skutkuję następującą interakcją z BD: pobraniem A z id=2, pobraniem odpowiadającej kolekcji B, usunięcie wszystkich B przypisanych do danego A, wstawienie kolejno poprzednio usuniętych B. - oraz wykorzystanie immutable list przy mapowaniu:
public class A{
@OneToMany
public List getBs(){
Collections.unmodifiableList(bs);
}
public void setBs(List b){
this.bs = bs;
}
}
Operacja załadowanie A po identyfikatorze ma skutkować następującą interakcją z BD: pobranie A, pobranie odpowiadających B, usunięcie odpowiadających B,dodanie kolejno usuniętych przed chwilą B.
"Code Generation on the JVM" by Hamlet D'arcy. Kilka razy miałem przyjemność a raczej nie-przyjemność bycia na prezentacji, która omawiała różne narzędzia i metody związane z generacją kodu. Wszystkie one jednak bazowały na narzędziach do generowaniu kodu źródłowego(CORBA stub generation, WSDL2Java, Java Beans generation). Hamlet skoncentrował się na zupełnie innych narzędziach, a w szczególności na: Lombok, Spring Roo oraz paru innych wynalazkach do Groovy.
W jednym z projektów korzystam z Lombok i bardzo sobię to narzędzie chwalę. Jedyna rzecz która mnie bardzo irytuję to fakt, że pod eclipse (może pod innym IDE jest podobny problem) w przypadku dodawania przez eclipse (CTRL+1) nowej metody do klasy z annotacją Lombok dostajemy w edytorze błąd. Wynika to z tego, że eclipse wstawił nam sygnaturę nowo utworzonej metody w miejsce, w którym są wygenerowane, "niewidzialne" metody. Nie licząc tej niedogodności, samo narzędzie jest na prawdę super. Nie obyło się bez nieśmiertelnego przykładu z generowaniem getterów/setterów, aby pokazać bardziej zaawansowane możliwości: @Cleanup (w Java 7 będzie specjalna instrukcja związana z automatic resource management), val ( final local variables oraz detekcja typu), @Synchronized (enkapsulacja locków wraz z generowaniem ich jako puste Object[] by były serializowalne), @Log. W celu sprawdzania jakie zmiany zostały dokonane przez Lomboka można użyć javap lub delombok. Hamlet wspomniał o samym mechanizmie działania Lomboka, który opiera się na annotacji, oraz Eclipse Handler i Javac Handler. Podobno stworzenie własnych rozszerzeń nie jest specjalnie trudne a jako first-step warto zobaczyć tu.
Następne na tapetę poszło Spring roo, ale nie było już tak dokładnie omawiane jako Lombok. Chyba Hamlet nie do końca zna ten projekt i skończyło się na odpaleniu jakiegoś bardzo prościutkiego przykładu oraz wspomnieniu o tym, że roo działa w czasie kompilacji, korzysta z inter-type declarations oraz jest dostępny mechanizm push-in refactoring. Po przejściu projektów javowych, Hamlet przeszedł do swojego świata Groovy. Zaczęło się od Groovy transformations a już sama liczba dostępnych transformacji jest naprawdę imponująca. Dotyczą one: generowania kodu (np. @ToString, @Lazy (wraz z użyciem volatile tworzy inteligentny lock aby uniknąć dwukrotnego inicjalizowania), @TupleConstructor ), zarządzania współbieżnością (np. @Synchronized, @WithReadLock, @WithWriteLock), logowanie(np. @Log, @Slf4j) , ograniczaniem dostępu (np. @PackageScope), implementacji patternów (np. @Singleton, @Immutable, @Delegate) oraz wielu innych przypadków. Wszystkie z tych transformacji mają się opierać na implementacji interfejsu GroovyCodeVisitor, którego to metody są wykonywane przy przechodzeniu przez AST. Następnie Hamlet pokazał projekt CodeNarc do statycznej analizy kodu w groovy, który też opiera się na wzorcu visitor. Kolejny narzędziem był GContracts, który ma realizować idee desing by contract dla groovy. Pomimo tego, że projekt jest nowy, wygląda całkiem, całkiem. Ostatni był Spock, który jest frameworkiem do testowania napisanym w groovy. Na pierwszy rzut oka sama idea przypomina użycie Parameterized runner,w którym to dane podawane są w formacie wiki. Spock ma też podobno bardzo dobre wsparcie do mocków.