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