czwartek, 22 stycznia 2009

Encje, Managery i MBeany Część 2

Witam
Bez zbędnego gadania zapraszam do kolejnej części cyklu. Dzisiaj będą:

Managery

Są to beany warstwy EJB, poprawniej byłoby je nazwać Session Beanami (jakieś inne nazwy?).
Nazwałem je managerami ze względu na pewną konwencje nazewniczą, która nakazuje nazywać je np OsobaManager.
Dostarczają metod do komunikacji z bazą danych (zapis, odczyt, modyfikacje).
Tak jak poprzednio proponuje zrobić osobną paczkę (np. pl.dzmitrow.beany ).

Manager to tak naprawdę dwa pliki. Pierwszym jest interfejs:
@Local
public interface OsobaManagerLocal {

public void dodajUsera(Osoba osoba);
public void usunUsera(Osoba osoba);
public List<Osoba> szukajUserow(String login);

}
Od razu widać, że klasa, która będzie implementować ten interfejs będzie dostarczać możliwość dodawania, usuwania i szukania userów (encja Osoba).
Adnotacja @Local mówi, że implementacja metod może być wykorzystywana tylko przez inne klasy tego EAR-a (jeżeli się mylę poprawcie mnie).
Istnieje również @Remote dająca możliwość udostępniania metod. Wykonana przez nas implementacja będzie dostarczana przez serwer aplikacji pod odpowiednią nazwą JNDI.
Nie będę się rozpisywał na temat @Remote, gdyż temat zasługuje na osobnego posta.
Konwencja nakazuje zaznaczać w nazwie czy dany interfejs dotyczy zdalnego czy lokalnego użytku (stąd OsobaManagerLocal).

Przejdźmy do drugiego pliku, implementującego ten interfejs:
@Stateless
public class OsobaManager implements OsobaManagerLocal {

@PersistenceContext(unitName="ProjektEJB")
EntityManager em;
...
}
@Stateless jak sama nazwa wskazuje wskazuje na bez stanowość beana (nie posiada on żadnych pól!).
Są również @Statefull, ale nie mam z nimi żadnego doświadczenia więc je pominę.
Zawiera jedynie definicje metod umożliwiających komunikacje z bazą. Poza warstwą EJB udostępnione są tylko te metody, które są podane w interfejsie.
@PeristenceContext, z którego persistence unit (o ile mamy ich zdefiniowanych więcej) ma korzystać entity manager (nazwę zdefiniowaliśmy w persistence.xml).
EntityManager em; pobiera entity managera ze zdefiniowanego kontekstu.
Niżej powinny oczywiście się znaleźć podane w interfejsie metody.

Zaimplementujmy dodawanie userów:
@Override
public void dodajUsera(Osoba osoba) {
em.persist(osoba);
}
Widzimy, że została wykorzystana metoda entity managera persist. Powoduje ona zapisanie do bazy danych instancji encji (w tym wypadku encji Osoba).
Pojawiła się też adnotacja @Override, większości z nas powinna być znana z standardowej javy. Oznacza ona metodę jako implementacje metody z interfejsu (lub nadpisanie metody z klasy, po której dziedziczymy).
Dzięki temu możemy być pewni, że pozostanie spójność naszej implementacji z interfejsem.
Drobna uwaga. Przy tworzeniu obiektu osoba nie ustawiamy id! Zostanie on automatycznie dodany (o ile encja została tak skonfigurowana) przy zapisywaniu do bazy danych.

Zaimplementujmy teraz usuwanie:
@Override
public void usunUsera(Osoba osoba) {

Osoba o = em.merge(osoba);
em.remove(o);
em.flush();
}
Pojawiło się kilka nowych metod entity managera.
merge - bardzo ważna metoda gdyż odpowiada za kilka spraw. Po pierwsze odpowiada za wciąganie instancji encji
do kontekstu entity managera. To znaczy, jeżeli taki obiekt istnieje w bazie danych to nasza instancja będzie z nim skojarzona.
Po drugie, jeśli obiekt był już w kontekście i zostały wprowadzone w nim zmiany to zostaną one zapisane do bazy danych. W przeciwnym wypadku zostanie on stworzony (jednak nie zaleca się z korzystania z merge zamiast persist).
remove(obiekt) - usuwa obiekt z bazy danych. Obiekt musi być w kontekście!
flush() - niektóre operacje na bazie danych mogą być kolejkowane. Jeżeli chcemy być pewni by nasze polecenie zostało wykonane od razu wykonujemy flush() i wszystkie zmiany zostają zapisane.

Pozostało nam zaimplementować szukanie:
@SuppressWarnings("unchecked")
@Override
public List<Osoba> szukajUsera(String userName) {

Query query = em.createQuery("SELECT o FROM Osoba AS o where o.login like :login");
query.setParameter("login",userName+"%");

return query.getResultList();
}
lub może trochę mniej bezpieczny, ale krótszy kod:
@SuppressWarnings("unchecked")
public List<Osoba> szukajUsera(String userName) {
return (em.createQuery("SELECT o FROM Osoba AS o where o.login like '"+userName+"%'")).getResultList();
}
Znów coś nowego. Query jest to klasa pozwalająca na wykonywanie poleceń na bazie danych w języku PODOBNYM do SQL.
Przykładem może byc Hibernate-owy HQL (Hibernate Query Language). Opis jak konstruować zapytania można znaleźć np. w dokumentacji hibernate-a.
W naszym przykładzie zapytanie tłumacząc na ludzki mówi:" z tabeli zawierającej ENCJE Osoba (FROM Osoba) pobierz takie instancje (SELECT o), których login jest jak zawartość zmiennej login (like :login)".
Po czym za login podstawiamy userName+"%" query.setParameter("login",userName+"%");.
Dzięki +"%" zwrócone będą osoby, których loginy zaczynają się od zawartości zmiennej userName. Korzystanie z setParameter() może być trochę mniej wygodne niż drugi przykład, ale jest bezpieczniejsze gdyż zmienne w ten sposób wprowadzane zmienne są czyszczone z potencjalnie niebezpiecznych wartości. Chroniąc nas w ten sposób przed atakami typu "SQL Injection".
FROM Osoba AS o pozwala na odnoszenie się w zapytaniu do pól osoby przez o.nazwaPola.
Należy zapamiętać, że FROM odnosi się do nazwy encji, a nie do nazwy tabeli.
query.getResultList() zwraca wynik zapytania jako listę. Jako, że metoda ta zwraca List, a nie List<Osoba> kompilator domaga się sprawdzenia czy zwracanie jest poprawne.
Niestety nic sensownego nie wymyśliłem poza zmienną lokalną lub dodaniem @SuppressWarnings("unchecked") (co zostało zastosowane). Jeżeli ktoś ma lepszy pomysł proszę o komentarz.
Oczywiście nie zawsze trzeba zwracać listę. Można zawsze zwrócić pojedynczy wynik przez getSingleResult(); (zwracany jest obiekt klasy Object więc konieczne są rzutowania).
Istnieją jeszcze "Named Query", ale co to i jak tego używać pozostawiam na pastwę ciekawości czytelnika.

Na deser sprawa poruszona w poprzednim poście czyli inicjalizacja pól z fetch ustawionym na FetchType.LAZY.
W hibernacie wystarczy wykonać:
Hibernate.initialize(d.getProdukty());
W ten sposób zostanie zainicjalizowane pole produkty i będziemy mogli pobrać jego zawartość (jeżeli ktoś wie jak zrobić to w OpenJPA proszę pisać w komentarzach).
To by było chyba wszystko.
Jak zwykle zapraszam do komentowania.

2 komentarze:

  1. Jak już jesteśmy przy wypisywaniu obiektów z tabeli, to jak wypisać konkretne jej pole (konkretne pole klasy encji)? Natknąłem się na przykład wykorzystujący metodę find() oraz @TransactionAttribute(REQUIRED), ale to nie działa. Macie jakiś pomysł?

    OdpowiedzUsuń
  2. Query query = em.createQuery("SELECT o.imie FROM Osoba AS o where o.login='jakisLogin'");
    String imie = (String)query.getSingleResult();

    Lub po prostu pobrac obiekt cały i wybraną przez nas property.

    Jezeli mamy poprawnie zdefiniowane encje wszystko powinno działac.

    OdpowiedzUsuń