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