poniedziałek, 26 maja 2008

EJB3 - interceptory

Jedną z nowości w specyfikacji EJB w wersji trzeciej są interceptory, poprzez które w prosty sposób można zastosować w budowanym systemie paradygmat programowania zorientowanego aspektowo.
Interceptory w EJB3 mogą odnosić się zarówno do cyklu życia ziaren jak i ich metod biznesowych. Implementuje się je w postaci metod. Metody-interceptory mogą znajdować się w klasach ziaren jak i w klasach bazowych tych ziaren, ale także mogą się znajdować w odrębnych klasach (klasy interceptorów). Metody-interceptory mogą mieć dowolny poziom dostępu (public, package, protected, private), ale nie mogą być za to ani static, ani final.
Warto zaznaczyć, że implementacja interceptorów nie jest niczym ograniczona, gdyż można z nich wywoływać JNDI, JMS, inne ziarna, JDBC, oraz EntityManager'a.

1. InvocationContext

Metoda-interceptor do skutecznego działania musi mieć przede wszystkim kontekst wywołania, tj. instancję ziarna, informację o wywoływanej metodzie biznesowej oraz jej parametrów, a także rezultatu wywołania. Te dane można uzyskać poprzez obiekt InvocationContext, który jest przekazywany jako parametr do metody-interceptora.
Na szczególną uwagę zasługuje metoda InvocationContext.proceed(). Jej wywołanie powoduje przejście sterowania do następnego interceptora lub (jeśli jest wywołana w ostatnim interceptorze w łańcuchu) do metody biznesowej ziarna (jeśli jest to interceptor metod biznesowych).

2. Klasa interceptorów

Klasa interceptorów jest zwykłą klasą(nie musi być nawet oznaczona specjalną adnotacją), która po prostu posiada w sobie zdefiniowane metody-interceptory. Klasy te muszą posiadać publiczny bezargumentowy konstruktor.
Cykl życia interceptorów jest ściśle związany z cyklem życia ziarna do którego jest przypisany.
Klasy interceptorów można przypisywać do całego ziarna lub do metod biznesowych ziaren z osobna poprzez adnotację @Interceptors, który jako argument przyjmuje listę klas interceptorów.
W praktyce dołączanie klasy interceptorów wygląda mniej więcej następująco:
@Stateless
@Interceptors(pl.dwalczak.Interceptor1.class)
public class Ziarno {
...
  @Interceptors(pl.dwalczak.Interceptor2.class)
  public void metodaBiznesowa() {
  }
}

3. Interceptory cyklu życia ziaren

Aby dana metoda była interceptorem cyklu życia ziarna należy ją oznaczyć jedną z następujących adnotacji, odpowiednio do pożądanej fazy życia:   - @PostConstruct,   - @PostActivate,   - @PreDestroy,   - @PrePassivate Jeżeli metoda jest zdefiniowana w klasie ziarna, to powinna mieć następującą sygnaturę:
void <METHOD>()
Natomiast, jeżeli znajduje się w klasie interceptorów to powinna ona wyglądać tat:
void <METHOD>(InvocationContext)

4. Interceptory metod biznesowych

Aby dana metoda była interceptorem dla metod biznesowych, należy ją oznaczyć adnotacją @AroundInvoke.
W jednej klasie interceptora, ziarna, bądź klasie bazowej ziarna można tą adnotacją oznaczyć tylko jedną metodę, niemniej nie prowadzi to do ograniczenia, żeby metoda biznesowa ziarna miała tylko jeden interceptor. Wszystkie interceptory zdefiniowane w klasach bazowych, interceptorach podłączonych poprzez adnotację @Interceptors oraz ten zdefiniowany w klasie ziarna będą obowiązywać, a kolejność ich wykonania będzie zgodna z kolejnością ich deklarowania oraz hierarchią dziedziczenia klasy ziarna.
Metody oznaczone adnotacją @AroundInvoke są wykonywane na tym samycm stosie wywołania co metoda biznesowa. Taka metoda powinna zwracać wartość zwróconą przez metodę biznesową, oraz być zdolną do wyrzucenia wyjątku wyrzucanego przez metodę biznesową. Z tego powodu powinna mieć następującą sygnaturę:
Object <METHOD>(InvocationContext) throws Exception
Interceptor aby przekazać sterowanie dalej, tj. do następnego interceptora, bądź metody biznesowej, powinien wywołać metodę proceed na obiekcie InvocationContext.

5. Zasoby pomocnicze

Specyfikacja EJB3
The Interceptor Pattern

sobota, 24 maja 2008

JPA - relacje jeden-do-jednego

W niniejszym artykule przyjrzę się bliżej relacji jeden-do-jednego oraz mapowaniu jej w JPA.
Można powiedzieć, że relacja jeden-do-jednego jest szczególnym przypadkiem relacji jeden-do-wielu/wiele-do-jednego omówionej przeze mnie w artykule JPA - relacje jeden-do-wielu i wiele-do-jednego. Osoby nie zaznajomione z mapowaniem tejże relacji, zachęcałbym aby w pierwszej kolejności przeczytały wspomniany artykuł.
Relacja jeden-do-jednego jest również w pewien sposób związana z pojęciem obiektów wbudowanych omawianych przeze mnie w artykule JPA - obiekty wbudowane (komponenty). Otóż wspomniane obiekty wbudowane stosuje się zamiennie z relacją jeden-do-jednego w celu poprawienia wydajności.

1. Przykład - dziedzina problemu

Przykład do tego artykułu w dużym stopniu został zaczerpnięty z JPA - obiekty wbudowane (komponenty).
A oto jego model obiektowy:
Jest to układ dwóch klas User oraz Address, w którym User agreguje całkowicie Address. Warto dodać, że tego typu wiązanie, w którym istnienie jednego bytu jest całkowicie zależne od istnienia drugiego bytu, jest bardzo powszechne w relacjach jeden-do-jednego. Podobnie jak w relacji jeden-do-wielu/wiele-do-jednego możemy wyróżnić byt nadrzędny(User) oraz byt podrzędny(Address). Ponieważ dla jednego bytu nadrzędnego przypada tylko jeden byt podrzędny, to w celu uzyskania lepszej wydajności (kosztem normalizacji) często stosuje się obiekty wbudowane zamiast jawnej relacji jeden-do-jednego.
W relacyjnym świecie przykładowy model będzie prezentował się następująco:
Powyższy model ukazuje, że implementacja relacji jeden-do-jednego po stronie bazy danych jest bardzo podobna do implementacji relacji jeden-do-wielu/wiele-do-jednego, ale o tym nieco póżniej.

2. Dwukierunkowa relacja jeden-do-jednego

Dwukierunkowa relacja to taka, w której oba obiekty są wzajemnie świadome tego, że są w relacji i z jednego obiektu można nawigować/przejść do drugiego. Zanim jednak przejdę do omawiania mapowania tej relacji, to najpierw chciałbym napisać kilka zdań o bazodanowej realizacji.
Otóż w bazie danych implementuje się relację jeden-do-jednego przez dodanie klucza obcego do jednej z dwojga encji. Jest więc dość podobna do implementacji relacji jeden-do-wielu/wiele-do-jednego, w której to encja podrzędna posiada klucz obcy do encji nadrzędnej. Z tą różnicą, że w tym przypadku nie ma wymagania, żeby encja podrzędna była właścicielem wiązania i może nim być również encja nadrzędna. Przy czym aby faktycznie była to relacja jeden-do-jednego, i nie mogła się zdegenerować do relacji jeden-do-wielu/wiele-do-jednego, należy założyć uniqe constraint'a na kolumnie klucza obcego.
Przy mapowaniu relacji jeden-do-jednego fundamentalną rolę odgrywa adnotacja @OneToOne, którą należy oznaczyć pola-wiązania obiektów będących w tej relacji. Co ciekawe dostępne atrybuty dla tej adnotacji to suma atrybutów dostępnych dla adnotacji @OneToMany i @ManyToOne, co uwypukla podobieństwo relacji jeden-do-jednego do relacji jeden-do-wielu/wiele-do-jednego i jednocześnie uwydatnia wysoką symetrię wiązań w tej relacji.
Opcjonalnie można użyć adnotacji @JoinColumn, która posłuży do specyfikacji klucza obcego - nazwy kolumny oraz określenie unique constraintu na tej kolumnie. Wg. specyfikacji JPA domyślnie kolumna klucza obcego nazywałaby się [nazwa_drugiej_tabeli]_[nazwa_klucza_głównego_drugiej_tabeli] oraz by nie został założony unique constraint przy generowaniu schematu bazy.
Fragment mapowania klasy User:
@Entity
@Table(name="user")
public class User {
 @OneToOne(cascade=CascadeType.ALL, optional=false)
 @JoinColumn(name="addr_id", unique=true)
 private Address address;
 ...
}
Fragment mapowania klasy Address:
@Entity
@Table(name="address")
public class Address {
 @OneToOne(mappedBy="address")
 private User user;
 ...
}
Semantyka atrybutów adnotacji @OneToOne jest taka sama jak dla wspomnianych adnotacjach @OneToMany i @ManyToOne. Powstrzymam się od ponownego ich omawiania gdyż to już zrobiłem w artykule JPA - relacje jeden-do-wielu i wiele-do-jednego.
Warto zauważyć, że powyższe mapowania są poprawne przy założeniu, że właścicielem powiązania jest encja User. Nieco inaczej by jednak wyglądało gdyby właścicielem powiązania była encja Address. Przede wszystkim adnotacja @JoinColumn znaczyłaby by pole Address.user a nie User.address. Analogiczna zamiana spotkałaby również atrybut @OneToOne.mappedBy.

3. Jednokierunkowa relacja jeden-do-jednego

Jednokierunkowa relacja to taka, w której tylko jeden obiekt jest świadomy bycia w relacji z innym obiektem i tylko od niego można nawigować/przejść do drugiego obiektu.
Bazodanowa realizacja jednokierunkowej relacji jeden-do-jednego oraz jej mapowanie jest po części takie same jak w przypadku dwukierunkowej relacji. Z tą różnicą, że mapowanie określamy tylko dla jednej z dwojga encji, która musi być właścicielem wiązania - posiadać klucz obcy do drugiej encji. Co ciekawe przy jednokierunkowej relacji, zamiennie do adnotacji @OneToOne można użyć @ManyToOne - jak ktoś tak bardzo lubi sobie zaciemniać model ;) Co prawda @ManyToOne nie ma możliwości określania atrybutu mappedBy, ale w przypadku jednokierunkowej relacji użycie tego atrybutu i tak byłoby nie możliwe.

4. Zasoby pomocnicze

Specyfikacja JPA
JPA - relacje jeden-do-wielu i wiele-do-jednego
JPA - obiekty wbudowane (komponenty)

niedziela, 27 kwietnia 2008

JPA - relacje jeden-do-wielu i wiele-do-jednego

W niniejszym artykule chciałbym zaprezentować mapowanie relacji jeden-do-wielu i bliźniaczą do niej relację wiele-do-jednego. W tego typie relacji możemy rozróżnić byty na jeden byt nadrzędny - po jednej stronie relacji i dowolną ilość bytów podrzędnych - po drugiej stronie relacji.
Relacja ta jest o tyle szczególna, że w świecie relacyjnym jej implementacja jest punktem wyjściowym do implementowania relacji jeden-do-jednego i wiele-do-wielu.
Rozróżniamy następujące warianty relacji tego typu:
  - dwukierunkową relacje jeden-do-wielu/wiele-do-jednego: obiekt nadrzędny posiada kolekcję referencji do obiektów podrzędnych, które posiadają referencję do obiektu nadrzędnego;
  - jednokierunkowa relacja jeden-do-wielu: obiekt nadrzędny posiada kolekcję referencji do obiektów podrzędnych;
  - jednokierunkowa relacja wiele-do-jednego: obiekt podrzędny posiada referencję do obiektu nadrzędnego;

1. Przykład - dziedzina problemu

Tradycyjnie w celach pomocniczych posłużę się mało skomplikowanym przykładem. A oto jego model obiektowy:

Jest to układ dwóch klas Osoba oraz Kontakt. Ta ostatnia reprezentuje dane kontaktowe osoby takie jak nr telefonu, czy też adres email. Osoba agreguje całkowicie Kontatk'y (wiązanie typu kompozycja), co dodatkowo uwidocznia nadrzędność jednego bytu do drugiego. Oczywiście omawiana relacja dotyczy się również wiązań typu asocjacja jak i zwykłej agregacji, jednak wybrałem kompozycję, gdyż w modelu relacyjnym będzie się to wiązało z dodatkowym ograniczeniem not null.
W relacyjnym świecie przykładowy model będzie prezentował się następująco:

2. Dwukierunkowa relacja wiele-do-jednego/jeden-do-wielu

Po modelu obiektowym, w którym występują połączeniach między obiektami, bardzo często oczekujemy możliwości dwukierunkowej "nawigacji" między powiązanymi obiektami. W naszym przykładzie oznacza to, że oczekujemy możliwości przejścia od obiektu Osoba do powiązanych z nią obiektów Kontakt i odwrotnie.
Aby sprostać tym wymaganiom musimy odpowiednio mapować obie te klasy.
2.1. Mapowanie klasy Kontakt
Encja Kontakt jest właścicielem powiązania, gdyż posiada klucz obcy do encji Osoba.
@Entity
@Table(name="kontakt")
public class Kontakt {
 ... 
 private Osoba osoba;

 @ManyToOne(optional=false)
 @JoinColumn(name="oso_id") // domyslnie kolumna by sie nazywala osoba_oso_id
 public Osoba getOsoba() {
  return osoba;
 }
}
JPA wymaga jedynie, żeby w obiekcie podrzędnym oznaczyć referencje do obiektu nadrzędnego za pomocą adnotacji @ManyToOne. Ja jednak dodatkowo nadpisałem pewne domyślne ustawiania. I tak:
  - ustawiłem atrybut optional=false tej adnotacji, co oznacza, że referencja do Osoba musi być ustawiona, co zostanie odpowiednio wyrażane podczas automatycznego generowania schematu bazy danych przez dodanie ograniczenia not null;
   - poprzez adnotację @JoinColumn ustawiłem nazwę kolumny, która jest kluczem obcym do tabeli Osoba. Domyślnie by została użyta kolumna o nazwie [nazwa_tabeli_nadrzędnej]_[nazwa_klucza_głównego_tabeli_nadrzędnej];
2.2. Mapowanie klasy Osoba
@Entity
@Table(name="osoba")
public class Osoba {
 ...
 private List<Kontakt> kontakty;

 @OneToMany(cascade={CascadeType.PERSIST, CascadeType.REMOVE}, mappedBy="osoba")
 @Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
 public List<Kontakt> getKontakty() {
  return kontakty;
 }
 
 public void dodajKontakt(Kontakt kontakt) {
  if (null == kontakty) {
   kontakty = new ArrayList<Kontakt>();
  }
  kontakt.setOsoba(this);
  kontakty.add(kontakt);
 }
}
Dla obiektu nadrzędnego jest wymagane, aby oznaczyć kolekcje do referencji obiektów podrzędnych za pomocą adnotacji @OneToMany. Ponieważ jest to wiązanie dwukierunkowe i mapowanie do modelu relacyjnego zostało już określone w klasie Kontakt, to należy wyrazić to poprzez atrybut mappedBy tej adnotacji, któremu należy przypisać wartość będącą nazwą referencji (w obiekcie podrzędnym) do obietku nadrzędnego. Z atrybutu mappedBy należy skorzystać nie tylko, żeby nie musieć podwójnie określać fizycznych parametrów modelu relacyjnego, ale przede wszystkim dlatego, żeby uniknąć zdublowanych zapisów do bazy danych. Atrybut mappedBy jest niedostępny dla adnotacji @ManyToOne.
Poza tym mapowanie jest wyposażone w parametry określające działania kaskadowe, ale o nich napiszę trochę więcej przy omawianiu działań utrwalania i usuwania obiektów.
2.3. Utrwalanie obiektów w bazie danych
Utrwalania obiektów może być wykonywane w sposób rozdzielny lub kaskadowy.
Schemat rozdzielnego utrwalania obiektów w naszym przykładzie wyglądałby następująco. Najpierw utworzenie i zapis obiektu Osoba:
Osoba o = new Osoba();
em.persist(o);
Następnie utworzenie, powiązanie z osobą i zapis obiektu Kontakt:
Kontakt k = new Kontakt("nr tel.", "+48123124234");
o.dodajKontakt(k);
em.persist(k);
Tu zwrócę uwagę, że metoda dodajKontakt dodaje kontakt do kolekcji kontakty w obiekcie o jak i ustawia referencję osoba w obiekcie k.
Jeżeli chcemy utrwalić obiekt Kontakt w następnej transakcji, to trzeba mu ustawić odpowiednio referencje do obiektu Osoba. Przy założeniu, że będziemy dysponować tylko id osoby, to można wykonać następujące działania:
Kontakt k = new Kontakt("GG", "124234");
Osoba o = em.getReference(Osoba.class, osobaId);
k.setOsoba(o);
em.persist(k);
Dodam tylko, że manualne stworzenie instancji Osoba i ustawienie pola id nie zadziała.

Utrwalanie kaskadowe obiektów. Często istnieje potrzeba utrwalania całego drzewa obiektów, tj. obiektu nadrzędnego - korzenia drzewa (Osoba) i powiązanych z nim obiektów podrzędnych (Kontakt). JPA umożliwia zrobienie tego poprzez wykonanie operacji utrwalania na obiekcie nadrzędnym. Przy czym należy pamiętać o ustawieniu atrybut cascade na CascadeType.PERSIST w mapowaniu - tak jak zostało to zrobione w mapowaniu klasy Osoba dla property kontakty.
Działanie kaskadowe utrwalania obiektów można ustawić również po stronie obiektu podrzędnego, jednak wydaje się to być mało praktyczne.
2.4. Usuwanie obiektów z DB
Podobnie jak utrwalanie również i usuwanie obiektów może być rozdzielne lub kaskadowe.
Kaskadowe usuwanie obiektów jest analogiczne do kaskadowego utrwalania. Tzn. wykonując operację usunięcia obiektu głównego - korzenia drzewa obiektów, automatycznie zostaną usunięte powiązane z nim obiekty podrzędne. Tu należy pamiętać o ustawieniu atrybut cascade na CascadeType.REMOVE w mapowaniu - tak jak zostało to zrobione w mapowaniu klasy Osoba dla property kontakty. Implementacja JPA powinna usunąć obiekty podrzędne przed usunięciem obiektu nadrzędnego, żeby uniknąć naruszenia bazodanowych ograniczeń. Jednak usuwanie te nie musi być optymalne. Np hibernate'owa implementacja usuwa każdy obiekt z osobna po jego id, podczas gdy lepszym rozwiązaniem byłoby usunięcie wszystkich obiektów z jednej grupy jednym zapytaniem odwołując się do klucza obcego do bytu nadrzędnego.
Działania kaskadowego usuwania obiektów nie można ustawić po stronie obiektu podrzędnego.
W związku z usuwaniem obiektów podrzędnych w relacji jeden-do-wielu/wiele-do-jednego można wykorzystać rozszerzenie hibernate'owe - kaskadowe działanie delete-orphan. Otóż jeżeli w mapowaniu klasy nadrzędnej, kolekcję referencji do obiektów podrzędnych oznaczymy adnotacją @Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN}), to po usunięciu jakiegoś obiektu z tej kolekcji i utrwaleniu obiektu nadrzędnego, usunięty z kolekcji obiekt zostanie również usunięty z bazy danych.
o.getKontakty().remove(k);
em.persist(o);
Przy stosowaniu tego rozszerzenia należy pamiętać, że dla obiektu nadrzędnego choć raz utrwalonego, nie można podmieniać kolekcji obiektów podrzędnych, powiedzmy na nową instancje ArrayList, a próba utrwalenia takiego obiektu zwyczajnie się nie powiedzie. Jest tak, gdyż hibernate stosuje własne implementacje kolekcji - persistence collections, które pomagają w wykonaniu działania delete-orphan.

Usuwanie rozdzielne. Nie ma, żadnego problemu żeby usuwać obiekty podrzędne w sposób rozdzielny. Ponadto, bez użycia hibernate'owego rozszerzenia kaskadowego działania delete-orphan, jest to jedyny sposób aby z bazy danych pozbyć się pojedynczego obiektu podrzędnego.
Jednak zrezygnowanie z kaskadowego usuwania dla obiektów nadrzędnych jest mało atrakcyjne. Jeżeli jest założone ograniczenie bazodanowe, że klucz obcy do encji nadrzędnej nie może być pusty, to taka operacja w ogóle się nie powiedzie. Generalnie co się z tym stanie dalej, zależy od ustawień działań kaskadowych w bazie danych.

3. Jednokierunkowa relacja wiele-do-jednego

W porównaniu do dwukierunkowej relacji wiele-do-jednego/jeden-do-wielu, klasa Kontakt i jej mapowanie pozostaje niezmienione, za to z klasy Osoba zostanie usunięte powiązanie do klasy Kontakt.
Takie podejście uniemożliwia konfigurację działań kaskadowych (poza tymi określonymi w schemacie bazy danych) wyzwalanymi przez operacje na obiekcie Osoba. Ewentualnie można określić działania kaskadowe wyzwalane przez operacje na obiekcie Kontakt.
W omawianym przeze mnie przykładzie jednokierunkowa relacja wiele-do-jednego nie ma raczej praktycznego zastosowania.

4. Jednokierunkowa relacja jeden-do-wielu

W porównaniu do dwukierunkowej relacji wiele-do-jednego/jeden-do-wielu, to z klasy Kontakt zniknie powiązanie do klasy Osoba, natomiast klasa Osoba i jej mapowanie zmieni się następująco:
@Entity
@Table(name="osoba")
public class Osoba {
 ...
 private List<Kontakt> kontakty;

 @OneToMany(cascade={CascadeType.PERSIST, CascadeType.REMOVE})
 @Cascade( {org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
 @JoinColumn(name="oso_id", nullable=false)
 public List<Kontakt> getKontakty() {
  return kontakty;
 }
 
 public void dodajKontakt(Kontakt kontakt) {
  if (null == kontakty) {
   kontakty = new ArrayList<Kontakt>();
  }
  kontakty.add(kontakt);
 }
}
Jak widać zmieniła się metoda dodajKontakt, która nie zawiera już instrukcji wiązania obiektu Kontakt z obiektem Osoba. Ale co najważniejsze zmieniło się nieco mapowanie. Ponieważ klasa Kontakt nie zawiera już mapowania tej relacji, to:
  - zniknęła definicja atrybutu mappedBy w adnotacji @OneToMany;
  - do mapowania property kontakty dodana została adnotacja @JoinColumn, która podobnie jak w klasie Kontakt służy do określenia kolumny w encji Kontakt, będącego kluczem obcym do encji Osoba. W przeciwnym wypadku domyślnie zostałaby użyta tabela pośrednicząca, jak w relacji wiele-do-wielu, o nazwie [nazwa_tabeli_nadrzędnej]_[nazwa_tabeli_podrzędnej], która zawierałaby dwa klucze obce do tych tabel o następującym schemacie nazw [nazwa_tabeli]_[nazwa_klucza_głównego].

Dla prezentowanego przeze mnie przykładu użycie jednokierunkowej relacji jeden-do-wielu jest całkiem rozsądnym podejściem. Jedyne co zostało utracone, to możliwość zapisu obiektów Kontakt niezależnie od obiektu Osoba. Utracona została również możliwość definiowania działań kaskadowych od strony obiektu Kontakt, ale do skorzystania z tej możliwości w tym przypadku i tak nie ma sensownej przesłanki.

5. Zasoby pomocnicze

Pliki źródłowe przykładu
JPA - pierwsze kroki
Hibernate Annotations - 2.2.5. Mapping entity bean associations/relationships
2.4. Hibernate Annotation Extensions
Specyfikacja JPA

poniedziałek, 14 kwietnia 2008

JPA - modelowanie dziedziczenia

Relacja dziedziczenia jest właściwa tylko dla modelu obiektowego i jako tako nie występuje w modelu relacyjnym. Z tego względu opracowane zostały podejścia do reprezentacji dziedziczenia w modelach relacyjnych:
  - jedna tabela na klasę konkretną (Single table per Concrete Class),
  - jedna tabela na hierarchię klas (Single table per Class Hierarchy),
  - złączenie podklasy (Joined Subclass).
Specyfikacja JPA, z użyciem adnotacji @Inheritance, umożliwia zastosowanie każdej z tych trzech strategii reprezentacji dziedziczenia.

Przykład - dziedzina problemu

Dla celów niniejszego artykułu zostanie wykorzystany poniższy model danych: Jest to prosta hierarchia klas, z których abstrakcyjna klasa Osoba jest klasą nadrzędną, a klasy OsobaPrawna i OsobaFizyczna z niej dziedziczą.

Jedna tabela na klasę konkretną (Single table per Concrete Class)

Wg. tej strategii model relacyjny prezentowałby się następująco: Te podejście przewiduje, iż na każdą klasę konkretną będzie przypadać jedna tabela w modelu relacyjnym. W naszym przypadku zostaną utworzone tabele dla klas OsobaPrawna i OsobaFizyczna. Przy czym tabele te będą zawierać również kolumny reprezentujące dziedziczone atrybuty. Tu trzeba zaznaczyć, że nie ma możliwości nadpisania mapowania dziedziczonych atrybutów - nie zadziała adnotacja @AttributeOverride(s).

Klasa Osoba nie będzie jawnie odwzorowana w modelu relacyjnym. Ta cecha sprawia, że polimorficzne asocjacje (tj. ogólne odwoływanie się do bytu Osoba, a nie do konkretnego rodzaju osoby) stają się nie możliwe, a polimorficzne zapytania bardzo trudne (w zapytaniach używa się unii, bądź preparuje się oddzielne zapytanie dla każdej podklasy).

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Osoba {
 ...
}
@Entity(name="osoba_prawna")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity(name="osoba_fizyczna")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Jedna tabela na hierarchię klas (Single table per Class Hierarchy)

Wg. tej strategii model relacyjny prezentowałby się następująco: W tym podejściu jednej hierarchii klas przypada tylko jedna tabela. W naszym przypadku oznacza to, że wszystkie klasy (Osoba, OsobaPrawna, OsobaFizyczna) będą reprezentowane tylko przez jedną tabelę. Tabela ta oprócz kolumn reprezentujących atrybuty tych klas, będzie posiadać kolumnę flagę (discriminator) - os_typ, która będzie określać jakiej klasy konkretnej jest dany rekord. Nie jest koniecznie definiowanie w klasie atrybutu reprezentującego discriminator'a, ale jak już jest to robione, to w mapowaniu tego atrybutu musi być wyłączona opcja zapisu i uaktualniania tej wartości, gdyż nie można jej zmieniać manualnie.

Te rozwiązanie nie posiada wad związanych z polimorfizmem, które posiadało poprzednie podejście. Jednak posiada inną wadę związaną z możliwościami definiowania ograniczeń kolumn (przedewszystkim not null) specyficznych dla klas konkretnych. Np. nie można wymusić żeby kolumna mapująca OsobaPrawna.regon nie mogła przyjmować wartości pustych, gdyż tego warunku nie sprosta żaden rekord reprezentujący obiekt OsobaFizyczna.

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity(name="osoba")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="os_typ")
public abstract class Osoba {
 ...
}
@Entity
@DiscriminatorValue(value="OP")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity
@DiscriminatorValue(value="OF")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Złączenie podklasy (Joined Subclass)

Wg. tej strategii model relacyjny prezentowałby się następująco: W tym podejściu każda klasa (nawet abstrakcyjna) ma w relacyjnym modelu swoją reprezentacje w postaci tabeli. Oznacza to, że dane konkretnego obiektu będą zapisane po części w tabeli klasy abstrakcyjnej oraz po części w tabeli klasy konkretnej. U nas np. atrybuty obiektu OsobaPrawna będą przechowywane w tabeli OsobaPrawna jak oraz w zakresie dziedziczonych atrybutów w tabeli Osoba.
Byt dziedziczący musi mieć odwołanie do bytu z którego dziedziczy. W roli tego odwołania może być wspólny klucz główny. I tak wiersz w tabeli klast OsobaPrawna będzie miał klucz główny równy co do wartości do klucza wiersza w tabeli klasy Osoba.

Te podejście nie ma wad poprzednich rozwiązań. Ponadto taki model relacyjny charakteryzuje się wyższym stopniem normalizacji w stosunku do konkurencyjnych rozwiązań. Co też może również być wadą tej strategii, gdyż wyższy stopień normalizacji oznacza, że dane są rozproszone w większej ilości tabelach, co zwiększa skomplikowanie zapytań i w pewnym stopniu czas ich wykonania.

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity(name="osoba")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Osoba {
 ...
}
@Entity(name="osoba_prawna")
@PrimaryKeyJoinColumn(name="op_id")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity(name="osoba_fizyczna")
@PrimaryKeyJoinColumn(name="of_id")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Dziedziczenie z klasy nie będącą encją

Specyfikacja JPA przewiduje dwa warianty zachowania w przypadku dziedziczenia z klasy nie będącej encją.
Wariant pierwszy - klasa bazowa nie jest oznaczona adnotacją @MappedSuperclass. W tym przypadku atrybuty odziedziczone są ulotne i nie będą zapisane w bazie danych po operacji utrwalenia obiektu.
Wariant drugi - klasa bazowa jest oznaczona adnotacją @MappedSuperclass. Wówczas dziedziczone atrybuty są trwałe wg. mapowania zdefiniowanego w tej bazowej klasie. Przy czym istnieje możliwość nadpisania tych mapowań poprzez użycie adnotacji @AttributeOverride(s).
Moim zdaniem mapowane klasy nadrzędne są ciekawym rozwiązaniem i można go użyć chociażby do definicji klasy bazowej wszystkich encji, która będzie zawierać atrybuty zawierające informacje o dokonywanych na encji zmianach.
Poniżej zamieszczam przykład użycia mapowanej klasy:
@MappedSuperclass
public class EncjaAudytowalna {
 private String uzytkownikModyfikujacy;
 private Date dataModyfikacji;
 
 @Column(name="audyt_uzytkownik")
 public String getUzytkownikModyfikujacy() {
  return uzytkownikModyfikujacy;
 }
 public void setUzytkownikModyfikujacy(String uzytkownikModyfikujacy) {
  this.uzytkownikModyfikujacy = uzytkownikModyfikujacy;
 }
 
 @Column(name="audyt_data")
 @Temporal(value=TemporalType.TIMESTAMP)
 public Date getDataModyfikacji() {
  return dataModyfikacji;
 }
 public void setDataModyfikacji(Date dataModyfikacji) {
  this.dataModyfikacji = dataModyfikacji;
 }
}
@Entity
@AttributeOverride(name="uzytkownikModyfikujacy", column=@Column(name="uz_au_um"))
public class Uzytkownik extends EncjaAudytowalna {
 private Long id;
 private String name;

 @Id
 @GeneratedValue(strategy=GenerationType.AUTO)
 public Long getId() {
  return id;
 }
 public void setId(Long id) {
  this.id = id;
 }

 @Column(name="nazwa")
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
}

Zasoby

Hibernate Annotations - 2.2.4. Mapping inheritance
Specyfikacja JPA

niedziela, 13 kwietnia 2008

Serializacja XML - zapis java.util.Date oraz wartości enumeracji

Niniejszy artykuł jest kontynuacją wątku z artykułu TestNG - dane testowe z plików XML o wykorzystaniu standardowej (J2SE) schemy serializacji XML objektów do preparowania danych testowych. Ten artykuł będzie traktował o zapisie danych typu java.util.Date oraz typu wyliczeniowego - enumeracji.
Gwoli przypomnienia w tym przypadku nie chodzi o serializację obiektów z poziomu javy, tylko o ręczne preparowania plików XML zawierających zserializowane obiekty, które zostaną zdeserializowane z użyciem standardowego mechanizmu - java.beans.XMLDecoder, a następnie zostaną użyte jako dane wejściowe dla testów jednostkowych.

Niemniej przy serializacji obiektu z poziomy javy, który ma jedno pole o typie java.util.Date a drugie o typie wyliczeniowym, uzyskamy efekt dalece niezadowalający, którego na pewno nie będziemy chcieli naśladować.
Pole o typie wyliczeniowym w ogóle nie zostanie zapisane. Z kolei pole z datom zostanie zapisane, ale w formie liczby milisekund od 1 stycznia 1970r. Na pewno nie jest to wygodna postać dla człowieka, anie do odczytu, a tym bardziej do zapisu. W każdym razie java.beans.XMLEncoder zserializuje java.util.Date w następujący sposób:
<void property="birthDate"> 
 <object class="java.util.Date"> 
  <long>439254000000</long> 
 </object> 
</void>
Not tak... to przecież całkiem logiczne, bo jest to jedyny nie "potępiony" sposób ustawiania daty jaki oferuje interfejs java.util.Date.
Normalnie (w javie) jakbym miał uzyskać datę ze stringa, to bym skorzystał z java.text.SimpleDateFormat. No ale jak go użyć w tym XML'u... już podaję:
<void id="sdf" class="java.text.SimpleDateFormat">
 <string>yyyy-MM-dd</string>
 <void id="date0" method="parse">
  <string>1983-12-03</string>
 </void>
</void>
...
<object class="pl.dwalczak.Osoba">
 ...
 <void property="dataUrodzin">
  <object idref="date0"/>
 </void>
 ...
</object>
Pierwsza część konstrukcji (znacznik void z id="sdf"), to deklaracja, której w javie odpowiada:
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
java.util.Date date0 = sdf.parse("1983-12-03");
Nie definiuje ona żadnego zserializowanego obiektu, a jedynie zmienne, które mogą być wykorzystane przy definiowaniu zserializowanych obietków. Może być umieszczona w elemencie głównym, lub też bezpośrednio w zserializowanym obiekcie.
Druga część konstrukcji (znacznik object) definiuje obiekt typu pl.dwalczak.Osoba, w którym ustawia pole dataUrodzin wcześniej zdefiniowaną zmienną date0.

Wracając do typu wyliczeniowego.
Załóżmy, że klasa pl.dwalczak.Osoba posiada pole typOsoby o typie pl.dwalczak.TypOsoby, który jest enumeracjom:
package pl.dwalczak;
public enum TypOsoby {
 OsobaFizyczna,
 OsobaPrawna;
}
Wówczas ustawienie pola typOsoby dla obiektu pl.dwalczak.Osoba może wyglądać następująco:
<void property="type"> 
 <object class="com.dwalczak.TypOsoby" field="OsobaFizyczna"/> 
</void>
Lub tak:
<void property="type"> 
 <object class="com.dwalczak.TypOsoby" method="valueOf">
  <string>OsobaPrawna</string>
 </object> 
</void>

Podsumowanie

O ile problem z serializowaniem wartości typu wyliczeniowego, nie specjalnie zmniejsza atrakcyjność formatu standardowej serializacji obiektów XML w J2SE, jako ogólnego formatu zapisywania danych testowych, gdyż manualnie można to zapisać w całkiem przyzwoitej formie. To problem z datą faktycznie obniża tą atrakcyjność, gdyż formuła zapisu jej w "ludzkiej" postaci jest trochę za długa i skomplikowana. Mimo to, myślę że stosowanie tego formatu może być mniej pracochłonne niż projektowanie własnego i pisanie kodu deserializującego z niego obiekty.

Zasoby

Long Term Persistence of JavaBeans Components: XML Schema
Sergey Malenkov's Blog - How to encode Type-Safe Enums?

poniedziałek, 7 kwietnia 2008

Testowanie ziaren EJB3 z użyciem TestNG

Po pierwszych potyczkach z JPA nadszedł czas na zdobycie pierwszych doświadczeń w pisaniu ziaren EJB3. Najpierw chciałem jednak wybadać sprawy związane z testowaniem tychże ziaren. W poprzednich wersjach EJB z testowaniem było dość ciężko, gdyż jak wiadomo ziarna, żeby działały musiały być osadzone w kontenerze EJB, które były dostępne tylko w serwerach aplikacji. Jak dla mnie, jest to zdecydowanie za skomplikowany i zbyt czasochłonny proces testowania. Niestety ziarna EJB3 również wymagają, żeby uruchamiać je w kontenerze EJB.

Wbudowywalny kontener EJB3

Od pewnego czasu JBoss udostępnia wbudowywalny kontener EJB3. Jest to kontener, który można uruchomić poza serwerem aplikacji. W chwili obecnej kontener ten ma co prawda pewne ograniczenia, ale póki udostępnia wszystkie wymagane przeze mnie usługi nie będę się tym zbytnio przejmował ;)
Niemniej koncepcja wbudowywalnego kontenera EJB3 umożliwia stworzenie, szybkiego i łatwego w użyciu środowiska testów jednostkowych dla ziaren EJB3.

Środowisko testowe EJB3

Przy tworzeniu środowiska testów jednostkowych dla ziaren wykorzystam wspomniany wbudowywalny kontener EJB3.
Do jego uruchomienia wymagane jest, aby w zasięgu classpath'a znajdowały się (oprócz implementacji tego kontenera i bibliotek zależnych) następujące pliki konfiguracyjne: embedded-jboss-beans.xml, ejb3-interceptors-aop.xml, jndi.properties, default.persistence.properties.
W celu uniknięcia umieszczenia tych plików we wszystkich podprojektach modułów EJB tworzących korporacyjną aplikację, jak i określania w każdym z nich zależności od jar'ów wbudowanego kontenera EJB3, postanowiłem stworzyć dedykowany projekt maven'a. Projekt środowiska testów jednostkowych EJB będzie zawierał wspomniane pliki konfiguracyjne, zdefiniowane zależności do jar'ów implementacji kontenera oraz bazową klasę testową, dla testów jednostkowych ziaren.
A oto implementacja bazowej klasy testów jednostkowych:
package pl.dwalczak.ejb3testenv;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.log4j.Logger;
import org.jboss.ejb3.embedded.EJB3StandaloneBootstrap;
import org.jboss.ejb3.embedded.EJB3StandaloneDeployer;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;

abstract public class Ejb3Test {

 private static final Logger LOG = Logger.getLogger(Ejb3Test.class);
 private static final String LOCAL_POSTFIX = "/local";
 private InitialContext initialContext;
 private EJB3StandaloneDeployer deployer;

 @BeforeSuite
 public void startupEjb3() {
  EJB3StandaloneBootstrap.boot(null);
  EJB3StandaloneBootstrap.scanClasspath();

  deployer = EJB3StandaloneBootstrap.createDeployer();
  try {
   deployer.setKernel(EJB3StandaloneBootstrap.getKernel());
   deployer.getArchivesByResource().add("META-INF/ejb-jar.xml");
   deployer.create();
   deployer.start();
   initialContext = new InitialContext();
  } catch (NamingException e) {
   LOG.error("Can't initialize context.", e);
   throw new RuntimeException(e);
  } catch (Exception e) {
   LOG.error("Deployer error.", e);
   throw new RuntimeException(e);
  }
 }

 @AfterSuite
 public void shutdownEjb3() {
  if (null != initialContext) {
   try {
    initialContext.close();
   } catch (NamingException e) {
    LOG.error("Can't close context.", e);
   }
  }
  if (null != deployer) {
   try {
    deployer.stop();
    deployer.destroy();
   } catch (Exception e) {
    LOG.error("Can't close deployer.", e);
   }
  }
  EJB3StandaloneBootstrap.shutdown();
 }

 @SuppressWarnings(value = "unchecked")
 public  T lookup(String beanName, Class businessInterface) {
  T result = null;
  try {
   result = (T) initialContext.lookup(beanName + LOCAL_POSTFIX);
  } catch (NamingException e) {
   LOG.error("Can't lookup bean: " + beanName, e);
   throw new RuntimeException(e);
  }
  return result;
 }
}
Dzięki temu przystosowanie nowego modułu EJB w maven'ie będzie polegało na dodaniu zależności do tego projektu (przy scope ustawionym na test).

Trzeba tu dodać, że bardziej złożona aplikacja korporacyjna będzie raczej wymagała specjalnie dostosowanego projektu środowiska testowego. Niemniej myślę, że przygotowany przeze mnie projekt tegoż środowiska jest dobrym punktem startowym.

Testowanie ziaren EJB3 - przykład

W moim przykładzie mamy do czynienia z bardzo prostym ziarnem:
package pl.dwalczak.ejb3pg1;
import javax.ejb.Stateless;
import org.apache.log4j.Logger;

@Stateless
public class CalculatorBean implements Calculator {
 
 private static Logger LOG = Logger.getLogger(CalculatorBean.class);

 public int add(int x, int y) {
  LOG.debug("add(" + x + ", " + y + ")");
  return x + y;
 }

}
Interfejs biznesowy:
package pl.dwalczak.ejb3pg1;

public interface Calculator {

 int add(int x, int y);
}
Po dodaniu zależności do projektu środowiska testowego można napisać test jednostkowy do tego ziarna, który może wyglądać w sposób następujący:
package pl.dwalczak.ejb3pg1;
import org.testng.Assert;
import org.testng.annotations.Test;
import pl.dwalczak.ejb3testenv.Ejb3Test;

public class CalculatorTest extends Ejb3Test {
 
 @Test
 public void testAdd() {
  Calculator calculator = lookup("CalculatorBean", Calculator.class);
  int sum = calculator.add(2, 2);
  Assert.assertEquals(sum, 4);
 }

}
Jest jeszcze tylko jedna kwestia. Do modułu EJB trzeba dodać plik konfiguracyjny META-INF/ejb-jar.xml, nawet jeśli nie trzeba w nim nic konfigurować. Jest to po prostu niezbędne, aby wykorzystywany kontener zaczytał klasy ziaren EJB3, pomimo że te są skonfigurowane z użyciem adnotacji.

Zasoby

Pliki źródłowe przykładu
Pliki źródłowe ejb3-test-env
Embeddable EJB 3.0
JSR-000220 Enterprise JavaBeans 3.0
JavaTM Platform Enterprise Edition, v 5.0 API Specifications

wtorek, 1 kwietnia 2008

TestNG - dane testowe z plików XML

Pisząc artykuł TestNG - dostawca danych przypomniałem sobie o pewnym mini framework'u do wykonywania testów jednostkowych, który przyszło mi kiedyś napisać. Musiałem go napisać nie dlatego, że w projekcie nie znano jUnita ;) ale dlatego, że jUnit w pewnych przypadkach nie wiele pomagał. Konkretnie chodzi o sytuację gdy wiele testów ma ten sam algorytm, a różnią się między sobą danymi testowymi (czasami są to dość duże zestawy danych).
Moim zdaniem w takim przypadku warto oddzielnie zarządzać danymi i algorytmami testowymi i umiejscowić je w różnych plikach. Wówczas istnieje możliwość, aby dane testowe przygotowywała inna osoba niż autor algorytmów testowych, nie trzeba ponownie kompilować testów gdy się zmieniły dane, no i łatwiej można uniknąć problemów z kodowaniem danych wejściowych.

DataProvider w TestNG nie wspiera bezpośrednio zaczytywania danych testowych z plików XML. Ale jak się okazało, robienie tego samemu nie jest trudne i co najważniejsze nie wymaga znaczących nakładów kodowania. Wystarczy zastosować dostarczaną przez specyfikacje J2SE serializację XML JavaBean'ów.
Schema serializacji jest relatywnie zrozumiała dla ludzi, a co najważniejsze jest bardzo elastyczna. Schema ta dopuszcza zapisywanie dowolnej liczby dowolnych obiektów. Ponadto można łatwo wygenerować przykładowy plik wejściowy do testów, poprzez zserializowanie spreparowanych w kodzie przykładowych danych.

Poniżej zamieszczam przykładową zawartość pliku XML z danymi testowymi:
<?xml version="1.0" encoding="UTF-8" ?>
<java version="1.5.0" class="java.beans.XMLDecoder">
 <array>
  <string>test1</string>
  <object class="pl.dwalczak.testngdp1.User">
   <void property="nickName">
    <string>heniek</string>
   </void>
   <void property="address">
    <object class="pl.dwalczak.testngdp1.Address"
     id="addr1">
     <void property="city">
      <string>Poznań</string>
     </void>
     <void property="postcode">
      <string>11-111</string>
     </void>
     <void property="street">
      <string>Jadwigi</string>
     </void>
     <void property="number">
      <string>11a/3</string>
     </void>
    </object>
   </void>
   <void property="mailingAddress">
    <object idref="addr1" />
   </void>
  </object>
 </array>
 <array>
  <string>test2</string>
  <object class="pl.dwalczak.testngdp1.User">
   <void property="nickName">
    <string>maniek</string>
   </void>
   <void property="address">
    <object idref="addr1" />
   </void>
   <void property="mailingAddress">
    <object idref="addr1" />
   </void>
  </object>
 </array>
</java>
Natomiast implementacja TestNG'owego data providera, korzystającego z tego pliku XML, może wyglądać następująco:
package pl.dwalczak.testngdp1;
import java.beans.XMLDecoder;
import java.io.FileInputStream;
import java.util.ArrayList;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

@Test
public class SimpleTest {
 
 @DataProvider(name="dp")
 public Object[][] createData() {
  ArrayList<Object[]> result = new ArrayList<Object[]>();
  XMLDecoder dec = null;
  try {
   dec = new XMLDecoder(new FileInputStream("testData.xml"));
   while (true) {
    Object o = dec.readObject();
    result.add((Object[]) o);
   }
  } catch (ArrayIndexOutOfBoundsException e) {
   // no more objects in stream
  } catch (Exception e) {
   throw new RuntimeException(e);
  } finally {
   if (dec != null) {
    dec.close();
   }
  }
  
  return result.toArray(new Object[result.size()][]);
 }

 @Test(dataProvider="dp")
 public void test(String arg0, User user) {
  System.out.println("arg0: " + arg0);
  System.out.println("user: " + user);
 }
}

Zasoby

Pliki źródłowe przykładu
Long Term Persistence of JavaBeans Components: XML Schema
Dokumentacja TestNG - 5.5.2 - From DataProviders