Pokazywanie postów oznaczonych etykietą jpa2. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą jpa2. Pokaż wszystkie posty

sobota, 10 listopada 2012

JPA2/Hibenrate - MultipleBagFetchException

Opisywałem już jedną niespodziankę, jaką szykuje nam hibenrate jeśli używamy java.util.List zamiast java.util.Set. Niestety to nie jest jedyna niespodzianka :). Podczas pracy z hibenratem możemy nagle trafić na taki oto błąd:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
wynikający z chęci pobrania co najmniej dwóch kolekcji z jednej encji. Oczywiście błąd ten dotyczy sytuacji w której korzystamy z java.util.List. Wystarczy w takim razie zmienić naszą kolekcję na java.util.Set i problem zostanie rozwiązany.

Chciwe pobieranie - krótkie wyjaśnienie na czym polega
Chciwe pobieranie (EAGER) polega na wybraniu wszystkich potrzebnych danych od razu. Możemy to zrobić na dwa sposoby:
  • dodania do adnotacji @OneToMany, @ManyToMany, itd. fetch=FetchType.EAGER. Trzeba uważać na pobranie zbyt dużej ilości danych. Więcej o tym problemie tutaj. W takiej konfiguracji dostaniemy nasz błąd już podczas deployu aplikacji.
  • podczas tworzenia zapytania, określić które wiązania/relacje mają zostać chciwie dobrane poprzez oznaczenie ich jako FETCH. W takiej konfiguracji nasz błąd zostanie odkryty w momencie uruchomienia zapytania.

środa, 9 maja 2012

Problem chciwego pobierania danych

Opis problemu Problem chciwego pobierania danych, czyli jak jednym zapytaniem pobrać połowę bazy danych. Sytuacja występuje, gdy nasza encja ma wiele wiązań ustawionych na łapczywe/chciwe pobieranie zależności (EAGER). W takim przypadku, gdy poprosimy hibenrata o pobranie jednej encji, to zostanie ona pobrana wraz ze wszystkimi zależnościami. Poniżej krótki przykład + sposób rozwiązania.

Opis problemu
Załóżmy że nasz mini system, będzie zarządzał zamówieniami. Dlatego tworzymy encję Person, która będzie reprezentowała nam osoby, oraz encję Order do zamówień. Dodatkowo dla wzmocnienia efektu, załóżmy że każda osoba, może posiadać wiele umów. W tym celu tworzymy jeszcze encję Agreement. Wszystko wygląda tak:
@Entity
@Table(name = "ORDERS")
public class Order implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private Date date;

 @OneToOne
 @JoinColumn(name = "seller_id")
 private Person seller;

 @OneToOne
 @JoinColumn(name = "buyer_id")
 private Person buyer;

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

 @Id
 @GeneratedValue
 private Long id;
 
 private String name;
 
 private String surname;
 
 @OneToMany(fetch=FetchType.EAGER)
 @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
}
Encje mapowane są w sposób standardowy. Jedynym udziwnieniem jest ustawienie chciwego pobierania w encji Person, dla pola agreements (wszystkie kolekcje domyślnie ładowane są leniwie).
Teraz tworzymy prostą metodę, która pobierze nam nasze zamówienie, i wyświetli krótki opis. W naszym uproszczonym przypadku chcemy wyświetlić tylko id i datę zamówienia. Metoda może wyglądać mniej więcej tak:
 // INNE METODY
 
 public void getOrderById(Long id) {
  OrderDto orderDto = null;

  orderDto = (OrderDto) session.createQuery("FROM Order WHERE id = :id")
    .setParameter("id", id).uniqueResult();
  System.out.println("ID: " + orderDto.getId());
  System.out.println("DATA: " + orderDto.getDate());
 }

 // INNE METODY
}
Po uruchomieniu, wyświetla nam się informacja o zamówieniu. Dla naszych testowych danych, wszystko działa bardzo szybko. Tak więc spójrzmy w logi:
Hibernate: select order0_.id as id0_3_, order0_.buyer_id as buyer3_0_3_, order0_.date as date0_3_, order0_.seller_id as seller4_0_3_, person1_.id as id1_0_, person1_.name as name1_0_, person1_.surname as surname1_0_, agreements2_.person_id as person3_1_5_, agreements2_.id as id5_, agreements2_.id as id2_1_, agreements2_.title as title2_1_, person3_.id as id1_2_, person3_.name as name1_2_, person3_.surname as surname1_2_ from ORDERS order0_ left outer join Person person1_ on order0_.buyer_id=person1_.id left outer join Agreement agreements2_ on person1_.id=agreements2_.person_id left outer join Person person3_ on order0_.seller_id=person3_.id where order0_.id=?
Hibernate: select agreements0_.person_id as person3_1_1_, agreements0_.id as id1_, agreements0_.id as id2_0_, agreements0_.title as title2_0_ from Agreement agreements0_ where agreements0_.person_id=?
ID: 3
DATA: 2012-05-04
Logi wyglądają co najmniej beznadziejne. Wydawało się że proste odwołanie się do encji Order, spowoduje wygenerowanie jednego zapytanie do tablicy ORDERS, a na konsoli mamy dwa zapytania, z czego jedno jest naprawdę dziwne. Na mojej konsoli wszystko wygląda mało czytelnie, wiec poniżej wklejam powyższe zapytania, po małej obróbce:
Hibernate: 

select order0_.id as id0_3_, order0_.buyer_id as buyer3_0_3_, order0_.date as date0_3_, order0_.seller_id as seller4_0_3_, person1_.id as id1_0_, person1_.name as name1_0_, person1_.surname as surname1_0_, agreements2_.person_id as person3_1_5_, agreements2_.id as id5_, agreements2_.id as id2_1_, agreements2_.title as title2_1_, person3_.id as id1_2_, person3_.name as name1_2_, person3_.surname as surname1_2_ 

from ORDERS order0_ 
left outer join Person person1_ on order0_.buyer_id=person1_.id 
left outer join Agreement agreements2_ on person1_.id=agreements2_.person_id 
left outer join Person person3_ on order0_.seller_id=person3_.id 

where order0_.id=?

Hibernate: 

select agreements0_.person_id as person3_1_1_, agreements0_.id as id1_, agreements0_.id as id2_0_, agreements0_.title as title2_0_ 

from Agreement agreements0_ 

where agreements0_.person_id=?
Teraz widzimy że pierwsze zapytanie to złączenie po 4 tablicach:
  • ORDERS czyli to co nas interesowało,
  • Person czyli wybranie kupującego dla zamówienia,
  • Agreement czyli wybranie wszystkich umów dla kupującego,
  • Person czyli wybranie sprzedającego.
W drugim zapytaniu wybieramy wszystkich umów dla sprzedającego. Dzięki temu jednym prostym zapytaniem pobraliśmy naprawdę dużo informacji. Teraz możemy tylko gdybać, co by było na bazie która jest o wiele większa od naszej testowej, oraz co się stanie jak nasze encje będą miały o wiele więcej wiązań.

Rozwiązanie
Chłopaki od hibernate niestety nie przewidzieli ładnego rozwiązania tego problemu, więc trzeba kombinować :/. Jednym ze sposobów jest zmiana wszystkich wiązań na LAZY i dobieranie ich w momencie gdy są nam potrzebne. Uwaga na n+1 select problems. Drugim (pseudo)rozwiązaniem jest wskazanie hibenratowi jakie pola mają zostać pobrane, a jakie nie. Możemy do tego celu stworzyć obiekty DTO (Data Transfer Object) lub w jakiś inny sposób zakomunikować jakie pola chcemy pobrać z bazy danych. O tych sposobach będę pisał w następnym poście (link później), w ramach tematu jak radzić sobie z problemem pobierania nadmiarowych danych (całych encji) a nie tylko kilku interesujących nas pól.


Jeśli macie jakieś inne pomysły, piszcie w komentarzach.

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