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

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.

środa, 21 stycznia 2009

Encje, Managery i MBeany Część 1

Witam
Zauważyłem ze niektórzy mają z tym problem więc postaram się przybliżyć jakie elementy powinny się znaleźć w encjach, managerach, i mbeanach (managery i mbeany w następnych postach).

Encje

Są one elementami warstwy EJB i warto przechowywać je w osobnym pakiecie (np pl.dzmitrow.projekt.encje).
W encjach umieszczamy definicje tabel i powiązania między nimi.
Tworzymy je jako zwykłe klasy javowe implementujące Serializable, z odpowiednimi adnotacjami.

Każda encja musi posiadać adnotacje @Entity i implementować interfejs Serializable
@Entity
public class Kategoria implements Serializable{
  private static final long serialVersionUID = 8211793733544613374L; /* dowolna liczba z L na koncu */
...
}
Oczywiście, by były przechowywane jakieś dane musimy zdefiniować odpowiednie pola np. wcześniej podana klasa Kategoria ma pola:
...
private int id;
private String nazwa;
private List<Produkt> produkty; /*definicja tej encji nie obchodzi nas w tym miejscu */
...
Pola powinny być prywatne ze względów bezpieczeństwa, a skoro są prywatne to nie ma do nich dostępu z zewnątrz bez setterów/getterów.
Możemy je wpisać ręcznie... , albo użyć Eclipsowego narzędzia. Aby wygenerować w otwartym pliku klikamy prawym w dowolnym miejscu i wybieramy Source -> Generate Getters and Setters.
Wybieramy pola dla których chcemy je utworzyć, klikamy OK i gotowe.

Musimy teraz zdefiniować, które pole jest kluczem głównym naszej encji. Zapewne jest to id więc przy getId() dodajemy:
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public int getId() {
  return id;
}
Ogólnie większość definicji w encji podaje się przy getterach i tylko one będą się pojawiać w poście.
Co zrobiliśmy? Powiedzieliśmy, że id ma być kluczem głównym (@Id) oraz klucze mają by unikalne i generowane automatycznie ( @GeneratedValue(strategy=GenerationType.IDENTITY) )

Możemy ustawić również unikalność pól. Dzięki czemu choćbyśmy chcieli nie będzie dwóch kategorii i takiej samej nazwie.
Są na to dwa sposoby. Pierwszy to:
@Entity
@Table(
name="Kategoria",
uniqueConstraints={@UniqueConstraint(columnNames={"id"}),@UniqueConstraint(columnNames={"nazwa"})})
public class Kategoria implements Serializable{
...
Przy czym warto zauważyć, że @UniqueConstraint(columnNames={"id", "nazwa"}) oznaczałoby unikalność pary a nie nazwy i id z osobna.
Drugi to dodać odpowiednią adnotacje przy getterze pola:
@Column(unique=true) /* takich opcji jest dużo więcej! */
public String getNazwa() {
  return nazwa;
}
Ważne jest by od razu zdefiniować wszystkie unikalności i inne właściwości, gdyż zmiany wejdą w życie tylko przy ponownym utworzeniu tabeli.

Jeszcze jedna drobna uwaga zanim przejdziemy do relacji między encjami.
Gdy tworzymy jakieś metody w encjach nie będące setterami/getterami, a nie chcemy dostawać warningów, należy oznaczyć je adnotacją @Transient będą one ignorowane przez EntityManager-a.

Teraz najgorszy koszmar definicje powiązań między encjami ;).
Do dyspozycji mamy następujące powiązania:
  • @OneToOne
  • @OneToMany
  • @ManyToOne
  • @ManyToMany
Definiujemy je jak poprzednio przy getterach np. w encji Osoba mogłoby się znaleźć coś takiego:
@OneToOne(cascade={CascadeType.ALL}, fetch=FetchType.LAZY)
public Adres getAdres() {
  return adres;
}
Rzecz jasna wystarczyłoby samo @OneToOne, ale przy okazji wytłumaczę co to jest fetch (cascade zostało wytłumaczone w tym poście).
fetch ma dwie możliwe wartości FetchType.LAZY i FetchType.EAGER . Są one odpowiednio predefiniowane dla pewnych połączeń, jednak nie będę teraz tu wszystkich wymieniał i odeśle do dokumentacji Hibernate-a.
Pierwszy pozwala na odroczenie pobrania danych pola do wybranego przez nas momentu, a drugi ustala by dane były pobierane od razu z całym obiektem. Przy czym przy używaniu Lazy trzeba w odpowiedni sposób zainicjalizować pole (o czym będzie w poście o managerach).

Gdy chcemy zdefiniować połączenie jednostronne @OneToMany warto zrobić tabele łączącą:
@OneToMany
@JoinTable(name="KATEGORIA_PRODUKT",
 joinColumns={@JoinColumn(name="KATEGORIA_ID")},
 inverseJoinColumns={@JoinColumn(name="PRODUKT_ID")})
public List getProdukty() {
  return produkty;
}
Gdy chcemy zdefiniować dwustronnie takie połączenie to (jak przy każdym dwustronnym) musimy zdefiniować stronę posiadającą.
Najłatwiej (w tym przypadku) jest zrobić jako stronę posiadającą @ManyToOne .
Jako, że chcę połączyć produkty z kategoriami wpierw strona trzymająca czyli produkty:
@ManyToOne(fetch=FetchType.EAGER)
@JoinColumn(name="kategoria_fk")
public Kategoria getKategoria() {
  return kategoria;
}
A w encji Kategoria:
@OneToMany(mappedBy="kategoria", fetch=FetchType.LAZY)
public List getProdukty() {
  return produkty;
}
Co to jest to mappedBy? Kompilator przez to rozumie, że definicja połączenia znajduje się po drugiej stronie (w encji Produkt) i jest określona dla pola kategoria. Fetch został ustawiony jako lazy bo pobieranie wszystkich produktów z danej kategorii zabiera sporo czasu a nie zawsze potrzebujemy mieć do nich dostęp.

W drugą stronę robi się zupełnie inaczej i ponownie odsyłam do dokumentacji hibernate-a.

Po zdefiniowaniu encji nigdzie więcej nie musimy ponownie ustawiać zależności i innych właściwości w managerach czy mbeanach.

Wszystkie adnotacje wykorzystane w tym poście są wytłumaczone i wszystkie ich właściwości podane w powyższej dokumentacji.
Zachęcam do komentowania i zadawania pytań.

piątek, 9 stycznia 2009

Kaskady pól Encji

Witam.
Każdy z nas pisząc encje z wykorzystaniem adnotacji, używał do określania relacji między nimi @OneToMany, @OneToOne i ustawiał kaskadę (jak w większości przykładów) cascade={CascadeType.ALL} (czyli przykładowo @OneToMany( cascade={CascadeType.ALL} ). Powoduje ona "przenoszenie" komend z obiektu, na którym wywołaliśmy polecenia EntityManger-a (persist, merge, remove) na zawartość pól kaskadowanych. Wszystko jest dobrze dopóki aplikacje i powiązania między encjami są proste. Jeżeli mamy na przykład sklepik z encjami Osoba i Bilet i ustawimy pełną kaskadowość. To przy usuwaniu osoby usuniemy też wszystkie bilety tej osoby. W tym wypadku wystarczy wyłączyć kaskadowość, ale w większych projektach może to być za mało.
Zobaczmy więc co za typy kaskadowania kryją się pod CascadeType.ALL.
Najważniejsze to:
  • CascadeType.PERSIST

  • CascadeType.MERGE

  • CascadeType.REMOVE

Pełna lista pod http://www.hibernate.org/hib_docs/annotations/reference/en/html_single/#entity-hibspec-cascade

Tak Więc jeśli chcemy by wraz z stworzeniem(w bazie) jakiegoś obiektu wiązało się z stworzeniem wszystkich jego pól "encyjnych" ustawiamy CascadeType.PERSIST gdy chcemy by update lub pobranie elementu powodowało update wszystkich pól, które są związane z encjami ustawiamy CascadeType.MERGE. Gdy chcemy ustawić kilka typów kaskadowania naraz wpisujemy je po przecinku. Przykładowo @OneToMany( cascade={CascadeType.MERGE, CascadeType.PERSIST})

W razie problemów z adnotacjami polecam wcześniej już podlinkowany poradnik do Hibernate-a http://www.hibernate.org/hib_docs/annotations/reference/en/html_single/. Będzie również pomocny przy korzystaniu z OpenJPA.
To tyle co miałem do powiedzenia jeżeli idzie o kaskadowanie.
Pytania (lub propozycje co do artykułów) pisać w komentarzach.
Pozdrawiam