poniedziałek, 30 kwietnia 2012

Hibernate - n+1 Select Problem

Opis problemu
n+1 Select Problem jest jednym z najbardziej popularnych problemów, z jakimi muszą zmagać się programiści wykorzystujących Hibernate. Problem ujawnia się tym, że w momencie gdy wyświetlamy listę pewnych obiektów na ekranie wraz z leniwie ładowaną kolekcją (LazyLoading), to Hibernate zamiast jednego zapytania, tworzy n+1 zapytań. Tzn jedno zapytania służy do wyświetlenia listy obiektów a kolejne n zapytań, do pobrania kolekcji dla każdego elementu z listy. Poniżej wyjaśnienie na małym przykładzie.

Przykład problemu
Mam mini programik, który magazynuje osoby z umowami. Każda osoba może posiadać wiele umów, natomiast każda umowa należy do jednej osoby. W związku z tym, mamy klasyczną relację jedne-wiele. Poniżej przykładowe mapowanie takich tablic na obiekty javy + adnotacje opisujące encję.
@Entity
public class Person implements Serializable {

 @Id
 @GeneratedValue
 private Long id;
 
 private String name;
 
 private String surname;
 
 @OneToMany
 @JoinColumn(name="person_id")
 private Set<Agreement> agreements;

 // METODY GET i SET
}
@Entity
public class Agreement implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String title;

 // METODY GET i SET
}

Teraz tworzymy metodę, która wyświetli nam wszystkie osoby wraz z umowami, jakie posiadają. Sposób wyświetlania nie ma wpływu na pokazanie problemu, dlatego ja użyję do tego prostej konsoli. W realnych aplikacjach będą to jakieś tabelki/panele która automatycznie będą iterować po wskazanej kolekcji. Poniżej prosta metoda wyświetlająca:
 // INNE METODY
 
 public void getPersonsWithAgreements() {

  List<Person> personList = session.createQuery("FROM Person p").list();

  for (Person person : personList) {
   System.out.println("Osoba: " + person.getName() + " : "
     + person.getSurname());
   System.out.println("----- UMOWY -----");
   for (Agreement agreement : person.getAgreements()) {
    System.out.println("Tytuł: " + agreement.getTitle());
   }
  }
 }

 // INNE METODY
Powyższa metoda wypisuje osobę, a pod osobą wszystkie umowy, jakie taka osoba posiada. Z pozoru nie ma tutaj żadnych błędów. Program się kompiluje, uruchamia a dla małej ilości danych testowych wyświetla wszystko w ułamku sekund. Tak więc można uznać że metoda działa i możemy zająć się jakimś innym zadaniem ... A jednak jest jeszcze coś na co powinniśmy zwrócić uwagę. Są to oczywiście logi. My, jako doświadczeni programiści spodziewamy się dane zostały pobrane jednego zapytania, które wyciągnie nam wszystkie osoby wraz z umowami. Tak więc zobaczmy jak wyglądają logi.
Hibernate: select person0_.id as id1_, person0_.name as name1_, person0_.surname as surname1_ from Person person0_
Osoba: Jan : Kowalski
----- UMOWY -----
Hibernate: select agreements0_.person_id as person3_1_1_, agreements0_.id as id1_, agreements0_.id as id0_0_, agreements0_.title as title0_0_ from Agreement agreements0_ where agreements0_.person_id=?
Tytuł: Umowa 1
Tytuł: Umowa 2
Osoba: Piotr : Nowak
----- UMOWY -----
Hibernate: select agreements0_.person_id as person3_1_1_, agreements0_.id as id1_, agreements0_.id as id0_0_, agreements0_.title as title0_0_ from Agreement agreements0_ where agreements0_.person_id=?
Tytuł: Umowa 3
Tytuł: Umowa 4
Tytuł: Umowa 5
Oczywiście interesują nas w szczególności logi Hibernata, czyli linijki w których pojawiają się zapytania. Widzimy że hibernate wygenerował nam jedno zapytanie, które pobiera nam wszystkie osoby (linijka 1), a następnie przy wypisywaniu umów generowane jest następne zapytanie (linijka 4) do wybrania umów dla konkretnej osoby. Kilka linijek niżej widzimy kolejne zapytanie (linijka 9), do pobrania umów następnej osoby. Sytuacja jest co najmniej zastanawiająca, ponieważ my spodziewaliśmy się jednego zapytania, a dostaliśmy 3:
  • jedno dla wybrania osób,
  • dwa dla wybrania umów dla dwóch osób.
Pytanie co by się stało jak by osób było 10,20,50,100? Pewnie wygenerowało by się odpowiednio 11/21/51/101 zapytań. Taka ilość zapytań jest mało wydajna w wypadku dużych aplikacji i może spowodować spore problemu wydajnościowe.

Jak sobie radzić z problemem n+1?
Wielu programistów uważa że Hibernate tak ma i nie ma co z tym walczyć. Hibernate jest prostym i fajnym narzędziem a jego skutkiem ubocznym są dziwne konstrukcje zapytań. Oczywiście nie sposób się nie zgodzić z tym że Hibernate jest "prosty" i fajny, ale akurat dziwne konstrukcje zapytań najczęściej generowane są z niewiedzy/lenistwa tych, którzy tworzą dany kawałek kodu. O prostocie Hibernate też można pewnie podyskutować :). Tak wiec chyba jednym słusznym rozwiązaniem, dla tego typu problemów jest pisanie dedykowanych zapytań w Hibernajcie z wykorzystanie JOIN FETCH. Czyli nie opieramy się na mechanizmach LazuLoadingu, które są wygodne ale słabo wydajne w sytuacji kiedy chcemy odwoływać się do zagnieżdżonych list. Poniżej zmodyfikowane metoda, która naprawia n+1 Select Problem.
// INNE METODY
 
 public void getPersonsWithAgreements() {

  List<Person> personList = session.createQuery(
    "SELECT DISTINCT p FROM Person p JOIN FETCH p.agreements").list();

  for (Person person : personList) {
   System.out.println("Osoba: " + person.getName() + " : "
     + person.getSurname());
   System.out.println("----- UMOWY -----");
   for (Agreement agreement : person.getAgreements()) {
    System.out.println("Tytuł: " + agreement.getTitle());
   }
  }
 }

 // INNE METODY
Jeszcze szybkie zerknięcie w logi:
Hibernate: select distinct person0_.id as id1_0_, agreements1_.id as id0_1_, person0_.name as name1_0_, person0_.surname as surname1_0_, agreements1_.title as title0_1_, agreements1_.person_id as person3_1_0__, agreements1_.id as id0__ from Person person0_ inner join Agreement agreements1_ on person0_.id=agreements1_.person_id
Osoba: Jan : Kowalski
----- UMOWY -----
Tytuł: Umowa 2
Tytuł: Umowa 1
Osoba: Piotr : Nowak
----- UMOWY -----
Tytuł: Umowa 4
Tytuł: Umowa 5
Widzimy że tym razem program zadziałał tak jak się spodziewaliśmy. Tzn. jednym zapytaniem wyciągnął wszystkie dane z bazy danych, a później tylko je wypisał.
Jeśli macie jakieś uwagi lub inne propozycje rozwiązywania tego problemu, piszcie w komentarzach.

środa, 11 kwietnia 2012

Hibernate - popularne problemy

Hibernate, to technologia ogólnie znana i lubiana. Zacznie ułatwia pracę z bazą danych, ale wiele osób nie zdaje sobie sprawy że niewłaściwe używanie powoduje bardzo poważne skutki. W najbliższym czasie postaram się rozwinąć problem bardziej, a że problem jest dość spory, rozbiję go na kilka postów: Jeśli macie inne propozycje, to piszcie w komentarzach. Postaram się w miarę możliwości wszystko powyjaśniać.