20 maja 2008

Dalsze atrakcje aplikacji JDA z JPA - @Enumerated

Kolejny dzień potyczek z aplikacją zbudowaną przy pomocy JDA (Java Desktop Application). Właśnie niedawno natrafiłem na ten skrót i zaskoczyła mnie zbieżność z JPA (Java Persistence API), którą również wykorzystuję w aplikacji Przychodnia. Jednym z zadań, jakie zaplanowałem w kontekście udoskonalania aplikacji była migracja pola plec typu boolean na typ wyliczeniowy (enum). Pamiętałem jeszcze z lektury specyfikacji i rozmów na grupach, że typy wyliczeniowe nie są czymś nadzwyczajnym dla JPA, więc nie spodziewałem się wielkich trudności, a mimo to i tak owa prostota rozwiązania zaskoczyła mnie.

Najpierw lektura specyfikacji JPA - rozdział 9.1.21 Enumerated Annotation (strona 181):

Domyślnie pole typu wyliczeniowego utrwalane (zapisywane) jest w bazie danych po jego liczbie porządkowej (wywołanie metody java.lang.Enum.ordinal()). Za pomocą adnotacji @Enumerated określany jest sposób mapowania pola typu wyliczeniowego, jako uzupełnienie dane z adnotacji @Basic (oczywiście adnotacja nie jest konieczna w wielu sytuacjach, więc najczęściej bazuje się na wartościach domyślnych adnotacji @Basic). Klasa java.lang.Enum, która reprezentuje wszystkie typy wyliczeniowe w Javie dostarcza dwie metody - ordinal() oraz name(), które uczestniczą w mapowaniu pola wyliczeniowego na struktury bazodanowe. Pierwsza z nich ordinal() wywoływana jest przy konfiguracji ORDINAL dla adnotacji @Enumerated, a druga przy wartości STRING. Domyślnie zakłada się ORDINAL, więc wszystkie typy wyliczeniowe będą zapisywane w bazie jako liczby porządkowe. Za pomocą STRING do bazy trafiają nazwy wartości typu wyliczeniowego.

Adnotacje posiadają swoje odpowiedniki w postaci elementów w pliku konfiguracji mapowania JPA, którym domyślnie jest META-INF/orm.xml (inne można wskazać za pomocą elementu mapping-file w META-INF/persistence.xml - "sercu" konfiguracji JPA lub nie wprost poprzez element jar-file). Odpowiednikiem xmlowym (zawartym w pliku orm.xml) adnotacji @Enumerated jest element enumerated (podelement basic), który akceptuje dwie wartości ORDINAL (domyślna) i STRING.

W mojej aplikacji Przychodnia zadeklarowałem typ wyliczeniowy pl.jaceklaskowski.przychodnia.model.Plec (początkowo był on w ramach klasy Pacjent, ale okazało się, że JDA narzekał na zagnieżdzenie klas wykorzystywanych do mapowania JPA).
 package pl.jaceklaskowski.przychodnia.model;

public enum Plec {

KOBIETA('K', "Kobieta"), MEZCZYZNA('M', "Mężczyzna");

private final char kod;
private final String nazwa;

Plec(char kod, String nazwa) {
this.kod = kod;
this.nazwa = nazwa;
}

public char kod() {
return kod;
}

@Override
public String toString() {
return this.nazwa;
}

public static Plec valueOf(int cyfra) {
return cyfra % 2 == 0 ? KOBIETA : MEZCZYZNA;
}

}
W pliku mapowania JPA - META-INF/orm.xml - wystarczyło dodać
 <basic name="plec" optional="false">
<enumerated>STRING</enumerated>
</basic>
aby do bazy trafiały napisy w postaci MEZCZYZNA lub KOBIETA. Nie rozwiązałem jeszcze kwestii zapisywania samych kodów typów, np. M dla mężczyzny, a K dla kobiety (ma ktoś pomysł jak to zrealizować, aby kod typu był wartością w bazie danych?).

Przy okazji zastosowania typu wyliczeniowego skorzystałem ze statycznego importu (import static) i kod stał się bardziej przejrzysty i czytelny. Wychodzę z założenia, że najpierw działający kod w wersji 1.0, a później jego uaktrakcyjnianie w kolejnych wersjach. W ten sposób podszedłem właśnie do zadania aplikacji Przychodnia.

Dodatkowo pola plec oraz wiek są ustawiane na podstawie obowiązkowego peselu, co sprowadziło się do następującej konstrukcji:
 protected void ustawWiekPlecNaPodstawiePeselu() {

// za Wikipedią o wieku w PESELu:
// http://pl.wikipedia.org/wiki/PESEL#P.C5.82e.C4.87
// Numeryczny zapis daty urodzenia przedstawiony jest w następującym porządku:
// dwie ostatnie cyfry roku, miesiąc i dzień.
// Dla odróżnienia poszczególnych stuleci przyjęto następującą metodę kodowania:
// * dla osób urodzonych w latach 1900 do 1999 - miesiąc zapisywany jest w sposób naturalny
// * dla osób urodzonych w innych latach niż 1900 - 1999 dodawane są do numeru miesiąca następujące wielkości:
// o dla lat 1800-1899 - 80
// o dla lat 2000-2099 - 20
// o dla lat 2100-2199 - 40
// o dla lat 2200-2299 - 60
int rokDzisiaj = Calendar.getInstance().get(Calendar.YEAR);
int rok = Integer.valueOf(this.pesel.substring(0, 2));
int miesiac = Integer.valueOf(this.pesel.substring(2, 4));
if (miesiac > 80) {
miesiac -= 80;
rok += 1800;
} else if (miesiac > 60) {
miesiac -= 60;
rok += 2200;
} else if (miesiac > 40) {
miesiac -= 40;
rok += 2100;
} else if (miesiac > 20) {
miesiac -= 20;
rok += 2000;
} else {
rok += 1900;
}
this.wiek = rokDzisiaj - rok;
if (this.wiek <= 0) {
throw new IllegalArgumentException("Niedozwolony wiek " + this.wiek + " [PESEL:" + this.pesel + "]");
}

// za Wikipedią o płci w PESELu:
// http://pl.wikipedia.org/wiki/PESEL#P.C5.82e.C4.87
// Informacja o płci osoby, której zestaw informacji jest identyfikowany,
// zawarta jest na 10 (przedostatniej) pozycji numeru PESEL.
// * cyfry parzyste (0, 2, 4, 6, 8) – oznaczają płeć żeńską
// * cyfry nieparzyste (1, 3, 5, 7, 9) – oznaczają płeć męską
this.plec = Plec.valueOf(Integer.valueOf(this.pesel.substring(9, 10)) % 2);
}
Coś mi mówi, że można byłoby to zrealizować prościej, ale nic na szybko nie przyszło mi do głowy, poza owym if-(else if)*-else. Sugestie mile widziane.

Niestety, sądziłem, że ta zmiana pociągnie za sobą zmiany w tworzonej dynamicznie tabeli przez JDA, tak że użytkownik będzie mógł wybrać płeć pacjenta z listy tworzonej na podstawie możliwych wartości typu wyliczeniowego, ale nic takiego się nie wydarzyło. W zamian pole stało się niemodyfikowalne, co w zasadzie jest równie dobre, gdyż jego wartość i tak wyliczana jest na podstawie obowiązkowego peselu.

Jutro zaplanowałem zabawę z formatowaniem danych w JTable podczas wyświetlania (renderery komórek) i edycji (edytory komórek).

3 komentarze:

  1. Jacku z moich doświadczeń z PESELem wynika, że popełniłeś "standardowy błąd numer 1", czyli zapisałeś numer pesel jako Integer. Znacznie łatwiej zapisać go jako String i wtedy analizować. Tak naprawdę w całym procesie nie trzeba ani razu wykonywać zamiany Integer-String, a jedynie wybierać poszczególne znaki z numeru pesel.

    OdpowiedzUsuń
  2. Pesel jest stringiem. Nawet korzystam z metody substring(). Skąd przypuszczenie, że mam go jako int?

    A wyciąganie danych z pesela poprawne? Coś wydaje mi się, że możnaby prościej.

    Jacek

    OdpowiedzUsuń
  3. if'y można "stablicować":

    int[] mieK = new int[]{0,20,40,60,80};
    int[] rokK = new int[]{1900,2000,2100,2200,1800};

    rok += rokK[miesiac/20];
    miesiac -= mieK[miesiac/20];

    miesiac może być max. 99 więc powinno działać ok. Oczywiście można dyskutować nad jakością tego pomysłu.

    OdpowiedzUsuń