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