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