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.

wtorek, 16 października 2012

Zasięg @ViewScoped w CDI/Weld

Szybki przepis jak w CDI/Weld stworzyć nowy zasięg @ViewScoped, ponieważ niestety nie został on domyślnie uwzględniony w specyfikacji. Weld oferuje nam 4 domyślne zasięgi:
  • @RequestScoped,
  • @ConversationScoped,
  • @SessionScoped,
  • @ApplicationScoped.
Po przejrzeniu tej listy od razu nasuwa się pytanie - gdzie jest @ViewScoped? Co prawda pojawił się nowy zasięg @ConversationScoped, który znany jest wszystkim tym, którzy mieli styczność z Seam'em, ale to chyba jeszcze nie powód, aby usuwać @ViweScoped (w Seam'ie - @Scope(ScopeType.PAGE)). Niestety z jakiś względów tak się właśnie stało. Na szczęście specyfikacja CDI pozwala nam tworzyć nowe zasięgi, co skłoniło mnie do tego, aby trochę poszperać i stworzyć zapomniany zasięg @ViewScoped. Swoją drogą autorzy dokumentacji Weld nie pomagają w tego typu czynnościach.

Potrzebny kod
Potrzebujemy dodać następujące klasy do naszego projektu.
package com.blogspot.mkorwel.context;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.enterprise.context.NormalScope;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

@Target(value = { METHOD, TYPE, FIELD })
@Retention(value = RUNTIME)
@NormalScope
@Inherited
public @interface ViewScoped {

}
package com.blogspot.mkorwel.context.spi;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;

public class ViewContextExtension implements Extension {

 public void afterBeanDiscovery(@Observes AfterBeanDiscovery event,
   BeanManager manager) {
  event.addContext(new ViewContext());
 }
}
package com.blogspot.mkorwel.context.spi;

import java.util.Map;

import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.faces.context.FacesContext;

import com.blogspot.mkorwel.context.ViewScoped;

public class ViewContext implements Context {

 public Class<ViewScoped> getScope() {
  return ViewScoped.class;
 }

 public <T> T get(Contextual<T> contextual,
   CreationalContext<T> creationalContext) {
  Bean<T> bean = (Bean<T>) contextual;
  Map<String, Object> viewMap =FacesContext.getCurrentInstance()
    .getViewRoot().getViewMap(true);
  
  if (viewMap.containsKey(bean.getName())) {
   return (T) viewMap.get(bean.getName());
  } else {
   T t = bean.create(creationalContext);
   viewMap.put(bean.getName(), t);
   return t;
  }
 }

 public <T> T get(Contextual<T> contextual) {
  Bean<T> bean = (Bean<T>) contextual;
  Map<String, Object> viewMap = FacesContext.getCurrentInstance()
    .getViewRoot().getViewMap(true);
  
  if (viewMap.containsKey(bean.getName())) {
   return (T) viewMap.get(bean.getName());
  } else {
   return null;
  }
 }

 public boolean isActive() {
  return true;
 }
}
W META-INF tworzymy plik: services/javax.enterprise.inject.spi.Extension, w którym zadeklarujemy użycie naszego zasięgu
com.blogspot.mkorwel.context.spi.ViewContextExtension
Użycie
Nasza "Managed Bean" o zasięgu @ViewScoped może wyglądać mniej więcej tak:
@ViewScoped
public class SomeControler implements Serializable {

 //some field and method
}

niedziela, 14 października 2012

Hibernate - Problem z listami przy relacji ManyToMany

Hibernate mimo swoich wielu zalet, ma również wiele wad :). Jedną z nich jest sposób obsługi java.util.List przy próbie mapowania @ManyToMany. W sumie przy pierwszym kontakcie wszystko działa bez zarzutu, ale w momencie gdy chcemy dodać coś do listy, w której są już jakieś inne elementy, to okazuje się, że jednak nie wszystko działa w 100% dobrze. W naszej konsoli widzimy mniej więcej taki log:
Hibernate: insert into Item (name, id) values (?, ?)
Hibernate: delete from OrderTab_Item where OrderTab_id=?
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Czyli w momencie wstawienia jakiegoś wiersza do listy, Hibenrate usuwa wszystkie relacje a następnie z powrotem je dodaje. W moim przykładzie dodajemy 1 element do listy już 3 elementowej. W związku z tym Hibenrate postanowił usunąć wszystkie relacji a następnie po kolei dodał całkiem nowe 4 wiersze. Przykład encji oraz kodu który który dodaje do encji widzimy poniżej.
Encje w relacji many-to-many.
@Entity
@Table(name="OrderTab")
public class Order implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
 private List items;

 // ... GET and SET
}
@Entity
public class Item implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 // ... GET and SET

}
Prosta metoda dodająca Item do Orderu.
public void addItem(long orderId, Item item) {

 Order o = em.find(Order.class, orderId);
 o.getItems().add(item);

 em.merge(o);

}
Rozwiązanie problemu
Wystarczy zmienić typ kolekcji w której przechowywujemy elementy z java.util.List na java.util.Set i wszystko będzie działać zgodnie z oczekiwaniami. Encja Order po zmianie wygląda tak:
@Entity
@Table(name="OrderTab")
public class Order implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
 private Set items;

 // ... GET and SET
}
a log w konsoli tak:
Hibernate: insert into Item (name, id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Pamiętajcie tylko że jak korzystacie ze zbiorów zamiast list, to należy doimplementować metody equals i hashCode.

wtorek, 4 września 2012

Joda Time - czyli koniec problemów z datą w javie

Joda Time, czyli darmowa biblioteka, dzięki której zapomnimy o mało przyjemnych operacjach na dacie w javie. Java mimo swojej ewolucji nie zadbała o porządną implementację klas reprezentujących czas i datę.
Na szczęście internet jest pełen darmowych bibliotek, które ułatwiają życie developerom. Jedną z nich jest Joda Time, czyli biblioteczka dostarczająca proste i funkcjonalne API do operacji na dacie. Odpowiednie jary potrzebne do uruchomienia Joda Time w projekcie można ściągnąć z oficjalnej strony.

Tworzenie
Pytanie - w czym Joda Time jest lepsza od standardowych klas Javowych? Odpowiedz - praktycznie we wszystkim :). Już przy tworzeniu można zauważyć mnogość konstruktorów. Nasz obiekt reprezentujący datę można stworzyć na kilkanaście sposób, m.in. można podać rok, miesiąc, dzień, itd. Dla przykładu standardowy obiekt java.util.Date posiada 2 konstruktory.
DateTime dt = new DateTime();
DateTime dt2000 = new DateTime(2000, 12, 7, 12, 0); //2000-12-07 12:00
Dodatkowo każdy stworzony obiekt jest immutable. Obiekty org.joda.time.DateTime możemy w łatwy sposób zamienić na obiekty java.util.Date i na odwrót.
// from Joda to JDK
DateTime dt = new DateTime();
Date jdkDate = dt.toDate();

// from JDK to Joda
dt = new DateTime(jdkDate);

Pobieranie danych
W standardowych obiektach typu java.util.Date, aby pobrać jakąś składową daty (rok, miesiąc, dzień, itp.), trzeba było tworzyć obiekt java.util.Calendar albo używać zakazanych/nieaktualnych (Deprecated) metod getDay(), getMonth(), .... Nowe API dostarcza wiele metod, które jednoznacznie wskazują co pobierają (w java.util.Calendar istniała magiczna metoda get która przyjmowała magiczny parametr typu int co ma zwrócić :) ). Kilka wybranych metod z org.joda.time.DateTime:
dt.getMonthOfYear();
dt.getDayOfWeek();
dt.getHourOfDay();
dt.getDayOfYear();
Modyfikacja daty
W standardowym podejściu należałoby stworzyć obiekt java.util.Calendar, który posiada ogólne metody add(int field, int amount), set(int field, int amount), itd. Zmiana czegokolwiek wymagała częściowej znajomości klasy java.util.Calendar, ponieważ parametry podajemy jako int'y, które co prawda są stałymi klasy java.util.Calendar, ale nadal są tylko int'ami. Korzystając z Joda Time nie musimy się takimi rzeczami przejmować. Po prostu należy użyć jednej z wielu metod dodających, odejmujących, itp.
dt.plusWeeks(1);
dt.plusMinutes(10);
dt.minusHours(3);
dt.minusYears(2);
Formatowanie daty
W javie mamy coś takiego jak DateFormat, dzięki któremu możemy nadać dacie taki wygląd, jaki potrzebujemy. Korzystając z Joda Time mamy dwa szybkie sposoby z użyciem bezpośrednio naszej klasy lub tworzeniem obiektu formatera:
// Sposób 1
System.out.println(dt.toString("yyyy-MM-dd"));

// Sposób 2
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ss");
System.out.println(formatter.print(dt));

poniedziałek, 23 lipca 2012

JEE6 i RESTful Web Services

Najnowsza specyfikacja Java EE 6 bardzo dobrze wspiera RESTful Web Services, czyli lżejszą wersję usług sieciowych. Poniżej demonstracja jak JEE radzi sobie z REST'ami.

Konfiguracja
Na początek warto sprawdzić czy mamy załączone odpowiednie liby. Jest to raczej formalność, ale nigdy nic nie wiadomo.

    org.jboss.spec.javax.ws.rs
    jboss-jaxrs-api_1.1_spec
    provided

Musimy również stworzyć klasę Activatora, która:
  • dziedziczy po javax.ws.rs.core.Application
  • posiada adnotację @javax.ws.rs.ApplicationPath z zadeklarowaną ogólną ścieżką do naszych REST'ów
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class JaxRsActivator extends Application {
   /* class body intentionally left blank */
}

Alternatywnie możemy umieścić kilka liniejek kodu w web.xml (opisane niżej).

RESTful Web Services
Klasa, która będzie wystawiać usługę typu REST powinna posiadać adnotację @javax.ws.rs.Path ze wskazaniem ścieżki pod jaką będzie widoczna usługa. Dodatkowo wskazujemy metodę, która będzie się wykonywać w momencie wywołania. Do tego celu wykorzystujemy adnotacje:
  • @GET (lub @POST, @HEAD, @PUT, itd.) z pakietu javax.ws.rs. Dzięki temu możemy określić różne metody dla różnych typów wywołań (GET/POST/PUT/HEAD/...) REST'a.
  • @javax.ws.rs.Produces z określeniem w jakim formacie będzie prezentowany wynik. Do dyspozycji mamy dwa popularne formaty: json ("application/json") i xml ("text/xml").
import java.util.List;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/hello")
public class HelloRestService {

 @Inject
 private HelloService helloService;

 @GET
 @Produces("application/json")
 public List<Person>> getPersons() {

  return helloService.getPersons();
 }

}
Tak stworzona usługa będzie widniała pod adresem http://127.0.0.1:8080/myApp/rest/hello.

Parametry
Jedno z podstawowych pytań, jakie warto zadać to: Jak przekazywać parametry do naszych usług? Odpowiedź jest bardzo prosta. Wystarczy stworzyć metodę z parametrami i wykorzystać kilka adnotacji. W kodzie wygląda to mniej więcej tak:
@GET
@Path("/{id}")
@Produces("application/json")
public Person getPerson(@PathParam("id") int id) {

 return helloService.getPerson(id);
}
W adnotacji @javax.ws.rs.Path wskazujemy jak w URL'u będą przekazywane parametry. Parametry oznaczamy w klamrowych nawiasach, np. {param1}. W ścieżce może pojawiać się statyczny tekst, np. "/personId/{id}". Kolejna przydatna adnotacja to @javax.ws.rs.PathParam, która wstawia parametry w odpowiednie miejsca w metodzie. Efekt możemy zobaczyć pod adresem: http://127.0.0.1:8080/myApp/rest/hello/1.

web.xml
Alternatywnie możemy umieścić całą konfigurację w web.xml. Jeśli ktoś ma potrzebę albo po porostu preferuje taki sposób konfiguracji, to wystarczy, że skopiuje poniższy fragment kodu. Oczywiście w takim wypadku nie potrzebujemy już klasy Activatora, możemy ją po prostu usunąć.
    
    Resteasy
    
            org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
     
     
            resteasy.servlet.mapping.prefix
            /rest
      
     1



       resteasy.scan
       true
   


        Resteasy
        /rest/*



XML
Wspomniałem już że Rest'y poza jsonem wspierają również XML. Niestety sprawa się trochę komplikuje, ponieważ obiekt, który ma zostać zamieniony na XML, wymaga małego tuningu :). Jeśli zwracane obiekty nie będą dostosowane, to w przeglądarce zobaczymy błąd 500, a w konsoli błąd:
Failed executing GET /hello/1: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: pl.mkorwel.sample.rest.app.rest.Person of media type: text/xml
Cały trick polega na dodaniu do klasy adnotacji @javax.xml.bind.annotation.XmlRootElement oraz stworzeniu bezargumentowego konstruktora. Klasa powinna wyglądać mniej więcej tak:
@XmlRootElement
public class Person {

 private String name;
 
 private String surname;

 public Person() {
 }

 public Person(String name, String surname) {
  super();
  this.name = name;
  this.surname = surname;
 }

 // GET i SET
}
Jeśli macie jakieś pytania/sugestie, zapraszam do umieszczania ich w komentarzach.

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

poniedziałek, 26 marca 2012

JSF - Managed Bean

Każdy kto pracuje z technologią JSF musiał spotkać się z Managed Beanami, czyli klasami Javy, za pomocą których możemy manipulować treścią na stronach xhtml. W niniejszym poście przyjrzymy się jak racjonalnie wykorzystywać MB do zarządzania treścią i nie popaść w skrajne patologie.

Logika biznesowa, skomplikowane operacje, dostęp do bazy, itp.
Należy pamiętać, że JSF to technologia za pomocą której tworzymy wygląd naszej aplikacji, czyli nie powinniśmy zaszywać w niej skomplikowanych obliczeń, dostępu do bazy danych, itd. Managed Beand jest częścią JSF i obowiązują go takie same reguły. 99% MB powinno być tylko pośrednikiem między tym co jest na stronach xhtml, a tym co jest w kodzie Java. Pytanie co z tym 1%?, zawsze znajdą się jakieś wyjątki :), ale jeśli twoja aplikacja składa się z samych wyjątków, to znaczy że coś jest nie tak.

Racjonalne wykorzystanie zmiennych
JSF i technologie pośrednie, mają kilka zasięgów do naszej dyspozycji:
  • @RequestScoped czyli standardowy request aplikacji. Wszystko co żyje w tym zasięgu jest dostępne tylko podczas jednego requestu.
  • @ViewScoped zasięg idealny dla AJAX'owych komponentów. Wszystkie komponenty w tym zasięgu są aktywne, do momentu kiedy nie przejdziemy na inną stronę. Zasięg nie jest dostępny dla JSF 1.2
  • @SessionScoped czyli zasięg sesyjny, idealny do trzymania wszelkich informacji do których często się odwołujemy (np. o zalogowanym użytkowniku)
  • @ApplicationScoped czyli zasięg aplikacyjny. Wszystko co w nim żyje jest dostępne przez cały okres życia naszej aplikacji.
Podczas tworzenia MB musimy określić jego zasięg. Przy wyborze któregoś z zasięgów powinniśmy kierować się jedną z najważniejszej zasad, czyli: "Nie używamy większego zasięgu niż jest nam potrzebny". Jeśli tworzymy listę osób/dokumentów/itp, nie musimy trzymać jej w sesji, wystarczy request (ewentualnie view, jeśli chcemy ją później modyfikować ajaxem). Innym przykładem mogą być informacje o zalogowanym użytkowniu / lub o koszyku z produktami, które ze względu na częste odwołania trzymamy w zasięgu sesji. Reqest w tym wypadku byłby niewydajny.

Gettery ...
Jedna z ważniejszych zasad jaka dotyczy JSF, to nie umieszczanie w metodach get, referencji do innych metod/obiektów realizujących jakąkolwiek logikę, ponieważ metody get wywoływane są przez silnik JSF kilkakrotnie podczas jednego requesta. Jeśli zapomnimy o tej zasadzie to nagle nasze skomplikowane metody przetwarzające dane (czyli najczęściej jest to jakiś select do bazy danych i/lub obróbka danych w javie) wywoływane są kilkakrotnie. Efektem takiego postępowania jest znaczne wydłużenie się ładowania strony. Strona, która powinna wczytać się w 3-4s, nagle wczytuje się w ponad 10 sekund, itd.

Oto krótki przykład- jak taki kod wygląda.
package pl.blogspot.mkorwel.myapp.view.faces;

import java.io.Serializable;
import java.util.List;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

import pl.blogspot.mkorwel.myapp.model.Person;
import pl.blogspot.mkorwel.myapp.service.PersonService;

@ManagedBean
@RequestScoped
public class PersonBrowser implements Serializable {

 //jakieś wstrzyknięcie
 private PersonService personService;

 public List<Person> getPersons() {
  return this.personService.getPersons();
 }

}

Jeśli ten kod przypomina wam wasze MB to znaczy, że trzeba zakasać rękawy i trochę popracować. Pierwszym pomysł na jaki wpadamy to przypisanie naszej listy osób do zmiennej i wywoływanie servicu tylko kiedy jest ona null'em. Po wprowadzeniu naszego hackerskiego tricku w życie, kod wygląda mniej więcej tak:

package pl.blogspot.mkorwel.myapp.view.faces;

import java.io.Serializable;
import java.util.List;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

import pl.blogspot.mkorwel.myapp.model.Person;
import pl.blogspot.mkorwel.myapp.service.PersonService;

@ManagedBean
@RequestScoped
public class PersonBrowser implements Serializable {

 // jakieś wstrzyknięcie
 private PersonService personService;

 private List<Person> persons;

 public List<Person> getPersons() {

  if (this.persons == null) {
   this.persons = this.personService.getPersons();
  }

  return this.persons;
 }
}
Tak przerobiony kod działa już po naszej myśli, tz. metoda z klasy servicowej wywołuje się tylko raz. Nasza strona znacznie przyspieszyła i wydaje się, że zadanie wykonane. Niestety po dłuższym namyśle nasz instynkt programisty podpowiada nam, że coś jest nie tak. Do rozwiązania tego problemu musimy sięgnąć trochę głębiej, a mianowicie do adnotacji @PostConstruct. Taką adnotacją wskazujemy metodę, która będzie wywołana na początku tworzenia się Managed Beanów (coś a'la konstruktor) i to właśnie w tej metodzie, powinniśmy zainicjalizować listę naszych osób. Po wprowadzeniu poprawek całość powinna prezentować się tak jak poniżej.

package pl.blogspot.mkorwel.myapp.view.faces;

import java.io.Serializable;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

import pl.blogspot.mkorwel.myapp.model.Person;
import pl.blogspot.mkorwel.myapp.service.PersonService;

@ManagedBean
@RequestScoped
public class PersonBrowser implements Serializable {

 // jakieś wstrzyknięcie
 private PersonService personService;

 private List<Person> persons;

 public List<Person> getPersons() {
  return this.persons;
 }

 @PostConstruct
 public void init() {
  this.persons = this.personService.getPersons();
 }
}

Jeśli macie jakieś inne ciekawe przygody związane z JSF, to zapraszam do dyskusji w komentarzach.