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...

poniedziałek, 27 lutego 2012

Musisz to zobaczyć!

Jeżeli jesteś developerem (a któż inny zaglądnął by w to miejsce) to na prawdę musisz to zobaczyć.
http://vimeo.com/36579366

Jak nazwiemy to "coś"? Example Driven Development? Feeling DD? TDD 2.0? What You See is What You Code?

Czekam na propozycje...

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

I nie mówcie, że zabawka użyta przy wizualizacji Binary Search to gadżet dla początkujących... wszyscy znamy hakierów, którym mogłoby się to przydać w codziennej pracy:P

A teraz czas sprawdzić czy nasz serwer EE skończył już redeploy...

poniedziałek, 30 stycznia 2012

Fantomowe tabelki w JSF

W niniejszym poście przyjrzymy się pewnej anomalii występującej w JSF, której skutki potencjalnie mogą być katastrofalne.
Anomalia została dobrze rozpoznana dosyć dawno, jednak jak pokazuje moje doświadczenie w pracy z zespołami korzystającymi z JSF, nie jest ona uświadomiona.
Dlatego dla tych z czytelników, którzy używają JSF i nie spotkali się ze zjawiskiem "klikania w nieodpowiednie wiersze tabelki" lektura posta jest obowiązkowa:)

Problematyczny scenariusz:
1. Użytkownik A wyświetla na stronie tabelkę z listą rekordów (Encje JPA lub DTO)
Na ekranie dla każdego rekordu mamy możliwość jego edycji/usunięcia poprzez Postback.

2. Podczas gdy użytkownik A delektuje się widokiem zaokrąglonych rogów na naszej stronie, użytkownik B (w tak zwanym międzyczasie) dokonuje w bazie zmiany danych, które są prezentowane na ekranie użytkownika A.
Może być to usunięcie danych lub edycja takich atrybutów, które wpłyną na ilość lub kolejność danych zwracanych przez zapytanie, które używa ekran użytkownika A.

3. Użytkownik A, po zaspokojeniu swych potrzeb estetycznych, klika przykładowo w pierwszy wiersz tabelki w celu usunięcia/modyfikacji rekordu.

4. Ku zdziwieniu użytkownika A, system poddał usunięciu/edycji zupełnie inny rekord niż zamierzony.

Zjawisko to pozwoliłem sobie nazwać Fantomową tabelką - ot jako żarcik techniczny będący paralelą (trudne słowo) do Anomalii Transakcji.

Przykład kodu

Managed Bean:
@ManagedBean()
public class UsersControler{ 
 
 @ManagedProperty("#{userFinder}")
 private UserFinder userFinder;
 
 @ManagedProperty("#{userManagement}")
 private UserManagement userManagement;
 
        //Bean o zasięgu sesji pamiętający nasze kryteria wyszukiwania
 @ManagedProperty("#{usersSearchCriteria}")
 private UsersSearchCriteria usersSearchCriteria;

 private List<User> users;
 
 private User selected;
 
 
 @PostConstruct
 public void search(){  
  users = userFinder.findUsers(usersSearchCriteria.getFirstname(), usersSearchCriteria.getLastname(), null, null);  
 } 

 public void remove(){
  userManagement.deleteUser(selected.getId());
  //search(); - zbędne dla zasięgu Request, konieczne dla View aby odświeżyć
 }
 
 public void remove2(Long id){ 
  userManagement.deleteUser(id);
  //search(); - zbędne dla zasięgu Request, konieczne dla View aby odświeżyć
 }   
}

Powyższy ManagedBean ma domyślny zasięg Request (nie chcemy przecież obciążać stanu sesji listą użytkowników).

Nasz bean ma wstrzyknięte 2 obiekty z warstwy aplikacji: UserFinder (wyszukujący użytkowników), UserManagement (operujący na użytkownikach - w przykładzie usuwamy użytkowników, ale problem jest ogólny - generalnie chodzi o jakąkolwiek modyfikację, która zmieni resultat działania UserFinder.findUsers )

Zwróćcie uwagę, iż kryteria wyszukiwania z formularza nie przechowywane w kontrolerze (który ma zasięg Request) a we wstrzykniętym Modelu o zasięgu Sesji.



    
     
     
      imie:  nazwisko: 
       
       
       
           
       
     
   
    
     ID
     #{_u.id}
    
 
    
    
     
      
      
     
                         
    
         
                
    

Widok jest bardzo prosty: tabelka iteruje po kolekcji użytkowników. Dla każdego z nich wyświetla ID (aby organoleptycznie zaobserwować problem) oraz umożliwia usunięcie (w ogólności modyfikację stanu bazy mającą wpływ na listę) użytkownika. Technicznie mamy tutaj dwa podejścia do przekazywania "klikniętego" użytkownika na Serwer: poprzez f:setPropertyActionListener oraz dzięki wywołaniu metody z parametrem.

To czy przekazujemy ID użytkownika czy obiekt klasy User nie ma znaczenia dla eksperymentu.

Dlaczego tak się dzieje

Załóżmy, że podczas pierwszego renderowania strony do pierwszego wiersza tabelki był "podpięty" pierwszy wiersz z wynikowej listy - użytkownik o id = 1.
Załóżmy, że później - po zmianie stanu bazy - pierwszy użytkownik na liście wynikowej pobranej z bazy ma id = 2.

Silnik JSF podczas postback odtwarza drzewo komponentów graficznych. Następnie "rozsmarowuje" na tym drzewie model. Tak więc podczas postback do pierwszego wiersza tabeli może podpiąć użytkownika o id = 2.

Model zdarzeń w JSF (zgodnie z nazwą Java Server Faces) odbywa się w kontekście serwera, czyli silnik ma informację o numerze klikniętego wiersza w tabelce (nie o id klikniętego usera). Dalej na podstawie klikniętego wiersza, zbiera z tego wiersza model i ten model traktuje jako "kliknięty". Jak widać - zakłada optymistycznie, że tak jest:)

Jeżeli przy postbacku model pobrany z bazy będzie inny - trudno... :P


Dlaczego często nie widać problemu

Programiści JSF często z powodu kłopotów z ogarnięciem złożoności zasięgów życia ManagedBeanów decydują się na rozwiązania "bo działa" i rozszerzają zasięg życia niemal wszystkich ich do Sesji. W przypadku zasięgu sesji nie odtwarzamy modelu danych na podstawie bazy lecz na podstawie ViewState zatem problem z klikaniem w fantomy jest po prostu ukryty - nie występuje. Czyli przy okazji, jako skutego uboczny niechlujstwa, zabezpieczamy się przez błędem fantomowych danych:)

Jeżeli zależy nam na zasobach lub świeżości danych, wówczas musimy wysilić się na rezygnację z Sesji gdzie tylko jest to możliwe i zaczyna się zabawa...

Rozwiązania

1. Zasięg View/Session
Przechowywanie list w Sesji to zwykle słaby pomysł, ale możemy zdecydować się na ich przechowywanie w zasięgu View, który trwa dopóki znajdujemy się na tej samej stronie (GET zrywa ten zasięg).
@ManagedBean()
@ViewScoped
public class UsersControler{

Można pokusić się o wydzielenie samego modelu prezentacji do osobnego ManagedBeana o zasięgu View tak aby nie przechowywać tam całego kontrolera, który jest zbędny i powinien żyć w Request. Wówczas wstrzykujemy model do kontrolera, napełniamy go w kontrolerze, po czym kontroler może już "umrzeć".


2. Ukryte Pole

Jeżeli nie możemy pozwolić sobie na przechowywanie stanu widoku, wówczas pewnym rozwiązaniem, jest rezygnacja z mechanizmów JSF do wskazywania klikniętego wiersza. Nasze buttony powinny przy pomocy JS ustawiać ukryte pole formularza na wartość id modelu wiersza i submitować to pole na managedbeana.


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

Po raz kolejny mamy do czynienia z sytuacją, gdzie próba ukrycia złożoności esencjalnej skutkuje wyciekiem jeszcze większej ilości złożoności przypadkowej.
"...życia nie oszukasz."

wtorek, 3 stycznia 2012

Dopamina Driven Development

Jaki wpływ na tworzenie kodu mogą mieć wytryski dopaminy?
Co wspólnego ma nikotyna z TDD?
Dowiecie się w 22. minucie i 30. sekundzie prezentacji Things I Wish I'd Known od samego twórcy Springa - Roda Johnsona:)


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

Generalnie bardzo polecam obejrzenie całej prezentacji, ale uprzedzam - nie ma w niej ani linijki kodu:)

piątek, 16 grudnia 2011

Skręt kości promieniowej i łokciowej, czyli rzecz o myszkach

Awaria mojej głównej myszki (czyli tej której używam do codziennej pracy, w odróżnieniu od specjalnej do gry ze znajomymi w CoD) skłoniła mnie do napisania kolejnego posta na temat Sprzętu.

Dawno, dawno temu, jako początkujący użytkownik peceta byłem przekonany, że jego głównym elementem, decydującym o ogólnym "ekspiriens" jest procesor i akcelerowana karta graficzna:) "Dziś sam jestem dziadkiem" i pytany przez nietechnicznych znajomych oraz nietechniczną rodzinę doradzam, że najważniejsze elementy zestawu to: krzesło, klawiatura, myszka i matryca (w sensie jej jakości) - reszta zależy już tylko od budżetu:)


Warto wiedzieć, że klasyczne "płaskie" myszki wymuszają nienaturalne (skręcone) ułożenie kończyny górnej i prowadzą do:
  • Skrętu wzajemnego położenia kości promieniowej i łokciowej w naszym przedramieniu,
  • Naprężeniu mięśni przedramienia (wynika z powyższego)
  • Ucisku na naczynia krwionośne (wynika z obu powyższych)

Jeżeli się nad tym chwilę nie zastanowić, to ułożenie dłoni na płaskim blacie tak aby do niego przylegała wydaje się normalne. Ale spróbujcie układać dłoń na zmianę na blacie: w pozycji "do spoliczkowania" i w pozycji "płaskiej", aby powoli zacząć uświadamiać sobie co się dzieje z Waszymi kośćmi i mięśniami.

Wielu z nas skarży się na bóle w okolicach nadgarstka. Ja sam w pewnym momencie miałem problem z utrzymaniem kierownicy w prawej ręce podczas powrotu z pracy do domu.
Jako rozwiązanie polecam tego typu konstrukcje.
Osobiście od ok 5 lat używam modelu Vertical Grip i w tym momencie każde zetknięcie mojej odzwyczajonej od tortur ręki z "płaską" myszką skłania do refleksji: ale po co?

UPDATE:
Przesiadłem się na Vertical Mouse 4, która jest jeszcze bardziej pionowa, ale jakością wykonania bije Vertical Grip na głowę. Na stronie (po prawej na górze) widać dobrze co dzieje się z kośćmi, o których pisałem wyżej.

ś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.