Projektowanie systemów informatycznych

Bliżej bazy danych, czyli o tym, czego uniknąć się nie da

część 9

Sebastian Wyrwał

Dotychczasowe rozważania, podobnie jak wiele książek
z zakresu projektowania systemów informatycznych, pomijały problem związany z
przechowywaniem danych
w bazie danych. Typowe książki dotyczące projektowania baz danych ograniczają
się natomiast do zagadnień związanych z zaprojektowaniem bazy danych i
korzystania
z tej bazy przy pomocy języka SQL.

Z drugiej strony oczywiste jest, iż większość
projektowanych aplikacji to systemy bazodanowe. O ile gra lub program edukacyjny
może przechowywać dane w pliku(ach), o tyle system wspomagający pracę firmy
powinien używać motoru bazy danych. Dysonans polega na tym, że systemy
projektuje się i implementuje z użyciem metod/języków obiektowych. Natomiast
prymat wśród baz danych wiodą bazy relacyjne. Niniejszy odcinek jest próbą
przybliżenia problematyki mapowania modelu relacyjnego na model obiektowy.

Zaprezentowano pobieżnie dwa rozwiązania służące do tego
celu. Pierwsze z nich to pakiet Hibernate. Jest on dość popularnym
produktem typu OpenSource dostępnym w internecie pod adresem
hibernate.sourceforge.net. Drugie z rozwiązań jest produktem firmy Oracle i
wchodzi w skład Oracle Application Server. Oba rozwiązania są uniwersalne,
tzn. mogą współpracować z bazami danych różnych producentów i mogą być
wykorzystane na różnych platformach systemowych. Oczywiście zaprezentowane
przykłady i opisy są bardzo proste i nie wyczerpują
w żaden sposób możliwości omawianych rozwiązań.

Wprowadzenie

W postępowaniu mającym na celu zaprojektowanie systemu
informatycznego (który jest realizowany obiektowo, ale korzysta z relacyjnej
bazy danych) można wyróżnić dwa kierunki:

  • Wyjście od modelu relacyjnego, który następnie będzie
    wykorzystany przez obiektową aplikację – tzn. najpierw powstaje projekt
    bazy danych, a później projekt aplikacji klienckiej [1]. W pracy [5] podano,
    że każdą, nawet nie relacyjną bazę danych należy projektować na poziomie
    logicznym, jako relacyjną i odpowiednio później przekształcając projekt.
  • Wyjście z modelu obiektowego, który następnie mapuje się
    na model relacyjny [7].

W niniejszym artykule zostanie omówione podejście drugie
jako bardziej naturalne. Zastosowanie podejścia pierwszego oznacza w zasadzie
odrzucenie metod projektowania obiektowego na rzecz metod strukturalnych i
wykorzystanie języka obiektowego do wytwarzania aplikacji klienta. W podejściu
drugim, samo zdefiniowanie i zaimplementowanie klas biznesowych nie nastręcza w
miarę doświadczonemu projektantowi i programiście zbytnich problemów.
Pojawiają się one natomiast przy mapowaniu modelu obiektowego na relacyjny:

  • W modelu relacyjnym nie ma dziedziczenia i
    polimorfizmu.
  • Na poziomie implementacji (model fizyczny) nie mogą
    istnieć związki typu „wiele do wielu” bez użycia dodatkowej
    tabeli.
  • W systemie relacyjnym potrzebne są klucze do
    identyfikowania encji [4,5].
  • Związki pomiędzy krotkami w tabelach w modelu
    relacyjnym są realizowane przez użycie kluczy obcych. W modelu obiektowym związki
    pomiędzy obiektami są wyrażane poprzez referencje [7] na poziomie języka
    programowania i poprzez powiązanie (ang. link) na poziomie modelu obiektowego.
  • Typ atrybutu klasy może być inny niż typ
    odpowiadającej mu kolumny [7]1.

Poza problemami wynikającymi z teoretycznych różnic pomiędzy
modelem relacyjnym i obiektowym, istnieją jeszcze pewne konsekwencje natury
praktycznej. System informatyczny wytwarzany jest zgodnie z innym, niż
wodospadowy, modelem cyklu życia oprogramowania. W kolejnych iteracjach system
taki ulega zmianom – są dodawane nowe klasy (encje), moduły, czy
wreszcie zmienia się zakres danych, które ma przechowywać system. Na skutek
tych zmian mapowanie klas (obiektów) na tabele komplikuje się dość znacznie.
Zmiany w fizycznym modelu danych pociągają zmiany w kodzie aplikacji
klienckiej [6], co możne być przyczyną różnych błędów i trudności.
Przejście z jednego modelu na drugi, bez wsparcia ze strony odpowiednich
bibliotek czy narzędzi, choć pracochłonne, jest możliwe. Dla zwiększenia
efektywności warto jednak używać gotowych rozwiązań.

Sposoby realizacji mapowania modelu obiektowego na model relacyjny

Jednym z głównych problemów jest mapowanie na model
relacyjny dziedziczenia, które jest kluczowe w metodologii obiektowej. W modelu
relacyjnym to ostatnie nie występuje. W pracy [7] wyróżniono następujące
rozwiązania tego problemu:

  1. zmapowanie całej hierarchii klas do pojedynczej tabeli,
  2. zmapowanie każdej nieabstrakcyjnej klasy do osobnej
    tabeli,
  3. zmapowanie każdej klasy (również abstrakcyjnej) z
    hierarchii do osobnej tabeli,
  4. zmapowanie hierarchii klas do uniwersalnego (generycznego)
    schematu.

Zaletą pierwszego z rozwiązań [7] jest prostota i szybkość
dostępu do danych. Wadami są: szybki rozrost tabel, możliwa propagacja zmian
w obrębie jednej klasy na inne klasy i nieefektywne wykorzystanie miejsca w
bazie danych. Ma ono zastosowanie do prostych hierarchii, gdzie nie zachodzi
zjawisko wielokrotnego dziedziczenia. Drugie
z rozwiązań [7] zapewnia – podobnie jak pierwsze – łatwy dostęp
do danych, ale zmiana w obrębie klasy (dotycząca atrybutów, które mają być
przechowywane) pociąga za sobą zmiany w tabeli. Ponadto zmiany w klasie
bazowej pociągają zmiany we wszystkich tabelach. Trudno jest zapewnić
integralność danych, jak również znacznie ograniczona jest możliwość pełnienia
różnych ról przez ten sam obiekt. Rozwiązanie to należy stosować, gdy nie
przewiduje się częstych zmian klas (typów) i nie ma wielokrotnego
dziedziczenia. Trzecie z rozwiązań jest koncepcyjnie łatwe, ale zapis i
odczyt danych może trwać dłużej [7], nietrudno natomiast dodawać nowe typy
i modyfikować istniejące. Rozwiązanie to można stosować również
w przypadku wielokrotnego dziedziczenia.

Ostatnie z rozwiązań [7] jest najbardziej elastyczne, ale
niestety najbardziej złożone. Jest ono bardzo uniwersalne leczz staje się
nieefektywne przy dużych ilościach danych.

Na marginesie tych rozważań należy stwierdzić, że choć
mapowanie odbywa się w ten sposób, że odpowiednikiem kasy jest tabela (lub
kilka tabel), to jest to niezgodne z teorią. W pracy [5] podano, iż
odpowiednikiem klasy jest dziedzina, a nie tabela.

Zagadnienia związane z mapowaniem modelu relacyjnego na
obiektowy zostaną zaprezentowane na przykładzie (uproszczonego) fragmentu
systemu do obsługi przychodni lekarskiej. Oczywiście, przedstawiony fragment
stosuje najprostsze z możliwych rozwiązanie, służące do definiowania
specjalności lekarza. Omawiany fragment jest jednak wystarczający do pokazania
podstawowych zagadnień związanych z mapowaniem – zawiera zarówno
dziedziczenie jak i relację asocjacji typu „wiele do wielu”.
Omawiany fragment systemu przedstawiono na rys. 1. Klasa Lekarz
dziedziczy z klasy Osoba. Lekarz ma dowolnie wiele specjalności,
jak również może istnieć dowolnie wielu lekarzy danej specjalności.


Rys. 1.
Lekarz i specjalności

Pakiet Hibernate – prosty przykład

Poniżej pokazano mapowanie klasy Osoba na tabelę w
relacyjnej bazie danych (przypominamy o niezgodności tego określenia z teorią!).
Zestaw atrybutów wynikający z opisu rzeczywistości rozszerzony został o
identyfikator (id). Metody w poniższej klasie służą do ustawiania i
pobierania wartości atrybutów. Klasa Osoba wygląda następująco (nie
uwidoczniono, z oszczędności miejsca metod get
i set dla pozostałych atrybutów):

package test;



public
class Osoba {

   

    
String id;

        String imie;

        String PESEL;

        String adres;

        String NIP;

        String numerTelefonu;

        String numerTelefonuKomorkowego;

        String nazwisko;

 

        public String getAdres() {

               
return adres;

        }

        public void setAdres(String string) {

               
adres = string;

        }

// analogiczne metody get i set dla wszystkich atrybutów

}

Dokument opisujący jej mapowanie w języku XML ma następującą postać:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping

        PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"

        "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

 

<hibernate-mapping>

 

    <class name="test.Osoba" table="Osoba">

 

        <id name="id" type="string" unsaved-value="null"
>

            <column name="id" sql-type="char(32)"
not-null="true"/>

            <generator class="uuid.hex"/>

        </id>

 

        <property name="imie">

            <column name="imie" sql-type="varchar(20)"
not-null="false"/>

        </property>

        <property name="PESEL">

            <column name="pesel" sql-type="varchar(20)"
not-null="false"/>

        </property>

        <property name="adres">

            <column name="adres" sql-type="varchar(50)"
not-null="false"/>

        </property>

        <property name="NIP">

            <column name="nip" sql-type="varchar(10)"
not-null="false"/>

        </property>

        <property name="numerTelefonu">

            <column name="numerTelefonu" sql-type="varchar(15)"
not-null="false"/>

        </property>

 

        <property name="numerTelefonuKomorkowego">

            <column name="numerTelefonuKomorkowego"
sql-type="varchar(20)" not-null="false"/>

        </property>

        <property name="nazwisko">

            <column name="nazwisko" sql-type="varchar(20)"
not-null="false"/>

        </property>

    </class>

</hibernate-mapping>

W powyższym dokumencie określono tabelę, która odpowiada
klasie. Dla każdego atrybutu określono odpowiadającą mu kolumnę w tabeli,
typ kolumny itd. Dokument taki tworzy się ręcznie, co jest jednak dość
proste. Poniżej znajduje się plik konfiguracyjny; określono w nim dane użytkownika
potrzebne do logowania, url serwera bazy danych, sterownik i dialekt języka
SQL.2 

hibernate.connection.username=nazwa_użytkowanika

hibernate.connection.password=hasło_użytkownika

hibernate.connection.url=url_serwera_bazy_danych

hibernate.connection.driver_class=sterownik

hibernate.dialect=dialekt_sql

(plik hibernate.properties)

Hibernate, dziedziczenie i asocjacja

Po bardzo prostej wprawce zostanie zaprezentowane rozwiązanie
implementujące fragment hierarchii klas z rysunku 1.3  Oczywiście, muszą
być zdefiniowane dwie pozostałe klasy. Podobnie jak w definicji klasy Osoba
nie ma dużego narzutu związanego z zastosowaniem omawianego rozwiązania.
Specjalności lekarza na poziomie kodu
w języku JAVA są przechowywane w zbiorze.

package test;

 

public class Specjalnosc {

        String id;

        String nazwa;

 

        public String getId() {

                   
return
id;

        }

//nie uwidoczniono pozostałych metod set i get

}

 

public class Lekarz extends Osoba{

 

        String gabinet;

        Set specjalnosci;

        //nie uwidoczniono metod get i set dla atrybutów

 

        public String toString(){

                   
return
getImie()+" "+getNazwisko()+" "+"
gabinet "+getGabinet();

        }

}

Poniżej zaprezentowano zmodyfikowany dokument mapujący dla
klas, będących przedmiotem przykładu. Dla klas będących w relacji
dziedziczenia (Osoba i Lekarz) zdefiniowano dyskryminator, który
pozwala na rozróżnianie typu obiektu.4  Kolumna, w której znajduje się
znacznik typu nazywa się typ. Dodatkowo zdefiniowano podklasę klasy Osoba,
którą jest klasa Lekarz. W obrębie znaczników wprowadzających powyższe
definicje (klasy Osoba
i Lekarz) zostały zdefiniowane wartości dyskryminatora dla tych klas.

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping

        PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"

        "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

 

<hibernate-mapping>

    <class name="test.Osoba" table="Osoba" discriminator-value="o">

        <id name="id" type="string" unsaved-value="null"
>

                   
<column name="id" sql-type="char(32)" not-null="true"/>

                   
<generator class="uuid.hex"/>

        </id>

 

        <discriminator column="typ" type="character"/>

 

        <subclass name="test.Lekarz" discriminator-value="l">

                   
<property name="gabinet">

                           
<column name="gabinet" sql-type="varchar(20)" not-null="false"/>

                   
</property>

                   
<set name="specjalnosci" table="lekspec" lazy="false">

                           
<key>

                               
<column name="lek_id" sql-type="char(32)" not-null="true"/>

                           
</key>

                           
<many-to-many class="test.Specjalnosc">

                               
<column name="spec_id" sql-type="char(32)"
not-null="true"/>

                           
</many-to-many>

 

                   
</set>

        </subclass>

 

</class>

 

<class name="test.Specjalnosc" table="Specjalnosc">

 

    <id name="id" type="string" unsaved-value="null"
>

            <column name="id" sql-type="char(32)" not-null="true"/>

            <generator class="uuid.hex"/>

    </id>

        <property name="nazwa">

            <column name="nazwa" sql-type="varchar(20)"
not-null="false"/>

        </property>

</class>

</hibernate-mapping>

W obrębie omawianego dokumentu znajduje się również
definicja klasy Specjalność. Poniżej pokazano skrypt definiujący
tabele dla powyższego przykładu. Warto jednak dodać, iż nie ma konieczności
pisania go ręcznie – pakiet Hibernate potrafi na podstawie pliku(ów)
definiujących mapowania zarówno wygenerować plik ze skryptem
w SQL, jak również potrafi stworzyć odpowiednie tabele w bazie danych.

create table lekspec (

    lek_id char(32) not null,

    spec_id char(32) not null,

    primary key (lek_id, spec_id)

)

create table Specjalnosc (

    id char(32) not null,

    nazwa varchar(20),

    primary key (id)

)

create table Osoba (

    id char(32) not null,

    typ CHAR(1) not null,

    imie varchar(20),

    pesel varchar(20),

    adres varchar(50),

    nip varchar(10),

    numerTelefonu varchar(15),

    numerTelefonuKomorkowego varchar(20),

    nazwisko varchar(20),

    gabinet varchar(20),

    primary key (id)

)

alter table lekspec add constraint FK39440CD88491C1F foreign key (spec_id)
references Specjalnosc

alter table lekspec add constraint FK39440CDBE0CBE68 foreign key (lek_id)
references Osoba

Poniżej zaprezentowano program testujący prezentowane rozwiązanie.
Na początku tworzone są obiekty odpowiadające trzem lekarzom oraz trzem
specjalnościom.
W tej części nie ma żadnego narzutu związanego z użyciem pakietu Hibernate.
Jego wykorzystanie rozpoczyna się od stworzenia obiektu klasy Configuration
i wywołaniu na jego rzecz metody addClass. Argumentem tej metody jest
nazwa klasy, której definicję mapowania należy wczytać. Dla klasy Osoba
będzie wczytany plik konfiguracyjny Osoba.hbm.xml. Nie ma potrzeby
rejestrowania innych klas, ponieważ definicje ich mapowań znajdują się również
w tym pliku. Następnie tworzony jest obiekt klasy SchemaExport
pozwala on na utworzenie w bazie danych potrzebnych tabel. Ich definicja wynika
z pliku mapowania. Po wykonaniu tych czynności zostają utworzone: fabryka
sesji, sesja i rozpoczęta zostaje transakcja. W obrębie tej ostatniej
utworzone wcześniej obiekty „lekarzy” i „specjalności
zostają zapisane do bazy danych. Lekarzom przypisane zostają specjalności, a
następnie dane odpowiadające lekarzom (w bazie danych) zostają uaktualnione.
W dalszej części zostaje utworzony obiekt zapytania. Po wykonaniu zapytania
transakcja zostaje zatwierdzona
a sesja zamknięta.

package test;

 

import java.util.HashSet;

import java.util.List;

import java.util.Set;

 

import net.sf.hibernate.Query;

import net.sf.hibernate.Session;

import net.sf.hibernate.SessionFactory;

import net.sf.hibernate.Transaction;

import net.sf.hibernate.cfg.Configuration;

import net.sf.hibernate.tool.hbm2ddl.SchemaExport;

 

public class Test {

 

    public static void main(String[] args)

                                       
throws
Exception {

 

    Lekarz l = new Lekarz();

    l.setNazwisko("Iksinski");

    l.setImie("Tadeusz");

    l.setAdres("23-315 Hibernacja, ul. Operatorów 1");

    l.setPESEL("1332");

    l.setNIP("3333");

    l.setGabinet("1233");

 

    Lekarz l1 = new Lekarz();

    l1.setNazwisko("Kazimierczyk");

    l1.setImie("Tomasz");

    l1.setAdres("33-315 Hibernacja, ul. Dermatologów 12");

    l1.setPESEL("1334");

    l1.setNIP("3323");

    l1.setGabinet("441");

 

    Lekarz l2 = new Lekarz();

    l2.setNazwisko("Wątrobowicz");

    l2.setImie("Jan");

    l2.setAdres("31-333 Hibernacja, ul. Dermatologów 12");

    l2.setPESEL("32");

    l2.setNIP("33332");

    l2.setGabinet("433");

 

    Specjalnosc s1=new Specjalnosc();

    s1.setNazwa("kardiolog");

    Specjalnosc s2 = new Specjalnosc();

    s2.setNazwa("alergolog");

    Specjalnosc s3 = new Specjalnosc();

    s2.setNazwa("dermatolog");

 

    Configuration cfg = new Configuration().addClass(Osoba.class);

 

    SchemaExport schema =new SchemaExport(cfg);

 

    schema.create(true,true);

    SessionFactory sessionFactory = cfg.buildSessionFactory();

 

    Session session = sessionFactory.openSession();

 

    Transaction transaction = session.beginTransaction();

 

    session.save(l);

    session.save(l1);

    session.save(l2);

    session.saveOrUpdate(s1);

    session.saveOrUpdate(s2);

    session.saveOrUpdate(s3);

 

    Set hs = new HashSet();

    hs.add(s1);

    l.setSpecjalnosci(hs);

    l2.setSpecjalnosci(hs);

 

    session.saveOrUpdate(l1);

    session.saveOrUpdate(l2);

 

    Query q = session.

    createQuery("from lekarz in class test.Lekarz where :spec in elements (lekarz.specjalnosci)");

    q.setParameter("spec",s1);

    List list = q.list();

 

    for (int i = 0;i<list.size();i++){

    System.out.println("lek "+(Lekarz)list.get(i));

    }

 

    transaction.commit();

    session.close();

}

}

Poniższe konstrukcje służą do stworzenia tabel w bazie
danych.

SchemaExport schema =new SchemaExport(cfg);

schema.create(true,true);

Warto zwrócić uwagę na postać zapytania w Hibernate Query
Language (brak słowa select jest zamierzony):

from lekarz in class test.Lekarz where :spec in elements(lekarz.specjalnosci)

Typowy schemat pracy z wykorzystaniem pakietu Hibernate wygląda
następująco:

  1. Zdefiniowanie klas biznesowych;
  2. Zdefiniowanie dokumentów opisujących mapowania;
  3. Zdefiniowanie pliku konfigurującego Hibernate – hibernate.properties.

Po wykonaniu powyższych czynności można (po dołączeniu
odpowiednich bibliotek w plikach jar) korzystać
z pakietu Hibernate.

Oracle TopLink

Dużo bardziej rozbudowanym przykładem rozwiązania
zapewniającego mapowanie relacyjno obiektowe jest Oracle TopLink, który należy
do Oracle Application Server. Samego mapowania dokonuje się zazwyczaj przy
pomocy wygodnego narzędzia z graficznym interfejsem użytkownika – Oracle
TopLink Mapping Workbench. Pozwala ono zarówno na wygenerowanie klas modelu
biznesowego, jak i na wczytanie klas stworzonych wcześniej przy pomocy innego
narzędzia. Na podstawie definicji klas można stworzyć definicje tabel.
Omawiane narzędzie potrafi (m.in.):

  • stworzyć tabele w bazie danych,
  • wygenerować skrypt, który utworzy tabele,
  • wygenerować kod w języku Java, przy pomocy którego można
    utworzyć tabele,
  • wygenerować opisy mapowania w języku XML,
  • wygenerować klasę (dziedziczą z klasy Project) w języku
    Java, która opisuje wszystkie mapowania.

Proste mapowanie może być zrealizowane automatycznie. Ręczne
też nie nastręcza problemów. Do wyboru są praktycznie wszystkie rodzaje
relacji (1:1, 1:n, m:n). Dokumenty opisujące mapowania w języku XML mogą być
stosowane zamiennie z klasą opisującą mapowanie. jednak w gotowym produkcie,
ze względu na szybkość wykonania, powinna być stosowana klasa opisująca
mapowania.

Zastosowanie plików z definicjami w języku XML pozwala
uniknąć częstego kompilowania aplikacji, co jest przydatne podczas prac związanych
np. z testowaniem czy wdrożeniem. Poniżej zaprezentowano klasę wygenerowaną
automatycznie przez narzędzie Oracle Top link5:

package test;

 

public class ProjectSource extends
oracle.toplink.sessions.Project {

 

public ProjectSource() {

    setName("ps9-top");

    applyLogin();

 

    addDescriptor(buildLekarzDescriptor());

    addDescriptor(buildOsobaDescriptor());

    addDescriptor(buildSpecjalnoscDescriptor());

}

 

public void applyLogin() {

    DatabaseLogin login = new DatabaseLogin();

    login.usePlatform(new
oracle.toplink.internal.databaseaccess.OraclePlatform());

   
login.setDriverClassName("oracle.jdbc.driver.OracleDriver");

    login.setConnectionString("jdbc:oracle:thin:@localhost:1521:MyDB");

    login.setUserName("scott");

    login.setEncryptedPassword("3E20F8982C53F4ABA825E30206EC8ADE");

 

    // Configuration Properties.

    login.setUsesNativeSequencing(true);

    login.setSequencePreallocationSize(1);

 

    login.setShouldBindAllParameters(false);

    login.setShouldCacheAllStatements(false);

    login.setUsesByteArrayBinding(true);

    login.setUsesStringBinding(false);

    if (login.shouldUseByteArrayBinding()) { // Can only be used with
binding.

        login.setUsesStreamsForBinding(false);

    }

    login.setShouldForceFieldNamesToUpperCase(false);

    login.setShouldOptimizeDataConversion(true);

    login.setShouldTrimStrings(true);

    login.setUsesBatchWriting(false);

    if (login.shouldUseBatchWriting()) { // Can only be used with batch
writing.

        login.setUsesJDBCBatchWriting(true);

    }

    login.setUsesExternalConnectionPooling(false);

    login.setUsesExternalTransactionController(false);

    setDatasourceLogin(login);

}

 

public Descriptor buildLekarzDescriptor() {

    oracle.toplink.descriptors.RelationalDescriptor descriptor = new
oracle.toplink.descriptors.RelationalDescriptor();

    descriptor.setJavaClass(test.Lekarz.class);

    descriptor.addTableName("OSOBA");

 

    // Inheritance Properties.

    descriptor.getInheritancePolicy().setParentClass(test.Osoba.class);

 

    // Descriptor Properties.

    descriptor.setAlias("Lekarz");

 

    // Cache Expiry Policy

 

    // Query Manager.

   
descriptor.getQueryManager().checkDatabaseForDoesExist();

    // Named Queries.

 

    // Event Manager.

 

// Mappings.

    DirectToFieldMapping gabinetMapping = new
DirectToFieldMapping();

    gabinetMapping.setAttributeName("gabinet");

    gabinetMapping.setFieldName("OSOBA.Gabinet");

    descriptor.addMapping(gabinetMapping);

 

    ManyToManyMapping specjalnosciMapping = new ManyToManyMapping();

   
specjalnosciMapping.setAttributeName("specjalnosci");

    specjalnosciMapping.setReferenceClass(test.Specjalnosc.class);

    specjalnosciMapping.useTransparentCollection();

   
specjalnosciMapping.useCollectionClass(oracle.toplink.indirection.Indirect-List.class);

   
specjalnosciMapping.setRelationTableName("Lek_Spec");

   
specjalnosciMapping.addSourceRelationKeyFieldName("Lek_Spec.lek_id",
"OSOBA.ID");

   
specjalnosciMapping.addTargetRelationKeyFieldName("Lek_Spec.spec_id",
"SPECJALNOSC.ID");

    descriptor.addMapping(specjalnosciMapping);

 

    return descriptor;

}

 

public Descriptor buildOsobaDescriptor() {

    oracle.toplink.descriptors.RelationalDescriptor descriptor = new
oracle.toplink.descriptors.RelationalDescriptor();

    descriptor.setJavaClass(test.Osoba.class);

    descriptor.addTableName("OSOBA");

    descriptor.addPrimaryKeyFieldName("OSOBA.ID");

 

    // Inheritance Properties.

   
descriptor.getInheritancePolicy().setClassIndicatorFieldName("OSOBA.Typ");

    descriptor.getInheritancePolicy().addClassIndicator(test.Osoba.class,
"O");

descriptor.getInheritancePolicy().addClassIndicator(test.Lekarz.class,
"L");

 

    // Descriptor Properties.

    descriptor.useSoftCacheWeakIdentityMap();

    descriptor.setIdentityMapSize(100);

    descriptor.useRemoteSoftCacheWeakIdentityMap();

    descriptor.setRemoteIdentityMapSize(100);

    descriptor.setSequenceNumberFieldName("OSOBA.ID");

    descriptor.setSequenceNumberName("OSOBA_SEQ");

    descriptor.setAlias("Osoba");

 

    // Cache Expiry Policy

 

    // Query Manager.

   
descriptor.getQueryManager().checkDatabaseForDoesExist();

    // Named Queries.

 

    // Event Manager.

 

    // Mappings.

    DirectToFieldMapping idMapping = new DirectToFieldMapping();

    idMapping.setAttributeName("id");

    idMapping.setFieldName("OSOBA.ID");

    descriptor.addMapping(idMapping);

 

    DirectToFieldMapping imieMapping = new DirectToFieldMapping();

    imieMapping.setAttributeName("imie");

    imieMapping.setFieldName("OSOBA.IMIE");

    descriptor.addMapping(imieMapping);

 

    DirectToFieldMapping nazwiskoMapping = new DirectToFieldMapping();

    nazwiskoMapping.setAttributeName("nazwisko");

    nazwiskoMapping.setFieldName("OSOBA.NAZWISKO");

    descriptor.addMapping(nazwiskoMapping);

 

    return descriptor;

}

public Descriptor buildSpecjalnoscDescriptor() {

    oracle.toplink.descriptors.RelationalDescriptor descriptor = new
oracle.toplink.descriptors.RelationalDescriptor();

    descriptor.setJavaClass(test.Specjalnosc.class);

    descriptor.addTableName("SPECJALNOSC");

   
descriptor.addPrimaryKeyFieldName("SPECJALNOSC.ID");

 

    // Descriptor Properties.

    descriptor.useSoftCacheWeakIdentityMap();

    descriptor.setIdentityMapSize(100);

    descriptor.useRemoteSoftCacheWeakIdentityMap();

    descriptor.setRemoteIdentityMapSize(100);

   
descriptor.setSequenceNumberFieldName("SPECJALNOSC.ID");

    descriptor.setSequenceNumberName("SPEC_SEQ");

    descriptor.alwaysConformResultsInUnitOfWork();

    descriptor.setAlias("Specjalnosc");

 

    // Cache Expiry Policy

 

    // Query Manager.

   
descriptor.getQueryManager().checkDatabaseForDoesExist();

    // Named Queries.

 

    // Event Manager.

 

    // Mappings.

    DirectToFieldMapping idMapping = new DirectToFieldMapping();

    idMapping.setAttributeName("id");

    idMapping.setFieldName("SPECJALNOSC.ID");

    descriptor.addMapping(idMapping);

 

    DirectToFieldMapping nazwaMapping = new DirectToFieldMapping();

    nazwaMapping.setAttributeName("nazwa");

    nazwaMapping.setFieldName("SPECJALNOSC.NAZWA");

    descriptor.addMapping(nazwaMapping);

 

return descriptor;

}

}

W konstruktorze powyższej klasy wywoływana jest metoda applyLogin,
a następnie tworzone są trzy deskryptory mapowań. W metodzie applyLogin
ustawiane są m.in. nazwa użytkownika, zaszyfrowane hasło, URL bazy danych,
sterownik, informacje dotyczące automatycznego generowania kluczy głównych
dla danych wstawianych do tabel. Generowanie kluczy może odbywać się dzięki
sekwencjom (trzeba je stworzyć w bazie danych) lub w oparciu o tabelę określoną
przez użytkownika (tabela taka ma dwie kolumny: nazwę sekwencji i licznik
– tworząc taką tabelę trzeba pamiętać o ustawieniu licznika na 0 dla
każdej nazwy sekwencji – narzędzie nie robi tego automatycznie).
Korzystając z sekwencji trzeba pamiętać o tym, aby parametr SequencePreallocationSize
miał wartość „jeden” – można to ustawić w kreatorze lub
„ręcznie w kodzie”.

Warto choć pobieżnie prześledzić definicję deskryptora.
Po utworzeniu obiektu RelationalDeskryptor ustawiana jest nazwa mapowanej
klasy, a następnie dodawana jest nazwa tabeli, na którą mapowana jest klasa
(w ogólności klasa może być mapowana na więcej niż jedną tabelę).
W przypadku klas, które są w relacji dziedziczenia, określane są oznaczenia
służące do rozpoznawania klas wchodzących w skład hierarchii, a następnie
pewne inne informacje dotyczące m.in. sekwencji, jakie będą używane przy
automatycznym generowaniu kluczy głównych.
W dalszej części są zdefiniowane mapowania dla poszczególnych atrybutów. Różnym
rodzajom mapowań odpowiadają różne klasy dziedziczące z klasy DatabaseMapping.

Poniżej pokazano prostą klasę testującą, która używa
przedstawionej przed chwilą klasy definiującej mapowania. Najpierw tworzony
jest obiekt klasy ProjectSource, która została wygenerowana
automatycznie przez narzędzie Oracle TopLink Mapping Workbench. Następnie
tworzona jest sesja i zostaje wywołana na jej rzecz metoda login. Zalecane
jest, aby operacje zapisu/uaktualniania obiektów były realizowane nie bezpośrednio
przy użyciu obiektu sesji, a przy pomocy obiektu klasy UnitOfWork. Po
stworzeniu omawianego obiektu, tworzone są obiekty odpowiadające lekarzom i
specjalnościom. Tworzone obiekty są od razu rejestrowane (przy użyciu metody registerObject).
Dalsze operacje wykonywane są na klonach zarejestrowanych obiektów, które są
zwracane przez metodę rejestrującą. Jeżeli obiekt nie zostanie
zarejestrowany, to nie ma możliwości zapisania lub uaktualnienia go.
Uaktualnienie odpowiednich bytów w bazie danych lub wstawienie nowych następuje
z chwilą wykonania metody commit klasy UnitOfWork. Obiekt klasy UnitOfWork
sam rozpoznaje, czy należy wykonać wstawienie (insert) czy tylko
uaktualnienie (update). W dalszej części kodu realizowane jest
zapytanie wybierające lekarzy gastrologów. Po wykonaniu zapytania następuje
wykonanie metod logout
i realase.

package test;

 

import java.util.List;

 

import oracle.toplink.expressions.Expression;

import oracle.toplink.expressions.ExpressionBuilder;

import oracle.toplink.sessions.DatabaseSession;

import oracle.toplink.sessions.Project;

import oracle.toplink.sessions.UnitOfWork;

import oracle.toplink.tools.workbench.XMLProjectReader;

 

public class Main {

    private Project project;

    private DatabaseSession session;

 

    public void run(){

        project = new ProjectSource();

        session = project.createDatabaseSession();

        session.login();

        UnitOfWork uow = session.acquireUnitOfWork();

        Specjalnosc gastrolog = (Specjalnosc)uow.registerObject(new
Specjalnosc("Gastrolog"));

        Specjalnosc kardiolog = (Specjalnosc)uow.registerObject(new
Specjalnosc("Kardiolog"));

        Specjalnosc alergolog = (Specjalnosc)uow.registerObject(new
Specjalnosc("Alergolog"));

        Lekarz kowalski=(Lekarz)uow.registerObject(new Lekarz ("Kowalski","Jan","518"));

        Lekarz kwiatkowski = (Lekarz)uow.registerObject(new Lekarz ("Kwiatkowski","Andrzej","518"));

        Lekarz iksinski =(Lekarz)uow.registerObject(new Lekarz ("Iksinski","Paweł","519"));

 

        kowalski.dodajSpecjalnosc(kardiolog);

        kowalski.dodajSpecjalnosc(gastrolog);

        iksinski.dodajSpecjalnosc(gastrolog);

        kwiatkowski.dodajSpecjalnosc(alergolog);

        uow.commit();

 

        //szukam gastrologów

        ExpressionBuilder expressionBuilder = new ExpressionBuilder (Lekarz.class);

 

        Expression expression = expressionBuilder.anyOf ("specjalnosci").get("nazwa")

        .equal("Gastrolog");

        List gastrolodzy = session.readAllObjects (Lekarz.class, expression);

 

        for (int i =0;i<gastrolodzy.size();i++){

        System.out.println(gastrolodzy.get(i));

        }

 

        session.logout();

        session.release();

    }

 

    public static void main(String[] args) {

        Main test = new Main();

        test.run();

    }

}

Poniżej zaprezentowano klasy, które zostały zastosowane w
przykładzie opisującym użycie rozwiązania TopLink firmy Oracle. Inaczej niż
w pakiecie Hibernate, nie jest konieczne definiowanie metod set/get dla
atrybutów, które mają być trwałe. Specjalności są przechowywane w klasie Vector
, która implementuje interfejs List.

package test;

 

import java.math.BigDecimal;

 

public class Osoba {

 

        private String imie;

        private String nazwisko;

        private BigDecimal id;

        private String typ;

 

public Osoba() {

        super();

}

 

public Osoba(String aString, String aString2) {

        nazwisko=aString;

        imie=aString2;

}

 

public BigDecimal getId(){

        return id;

}

 

public String toString(){

        return imie+" "+nazwisko;

}

 

}

 

package test;

 

import java.util.List;

import java.util.Vector;

 

public class Lekarz extends Osoba {

 

        private String gabinet;

        private List specjalnosci;

 

public Lekarz() {

 

        this.specjalnosci = new Vector();

}

 

public Lekarz(String aString, String aString2, String aString3) {

        super(aString, aString2);

        gabinet=aString3;

        this.specjalnosci = new Vector();

}

 

public String toString() {

               
return
"lek. "+super.toString()+"
"+gabinet+ " id "+getId();

}

public void dodajSpecjalnosc(Specjalnosc spec){

        specjalnosci.add(spec);

}

}

 

package test;

 

import java.math.BigDecimal;

 

public class Specjalnosc {

 

        private BigDecimal id;

        private String nazwa;

 

public Specjalnosc() {

        super();

}

 

public Specjalnosc(String aNazwa) {

        super();

        nazwa=aNazwa;

}

}

Poniżej przedstawiono dane znajdujące się w tabelach
Osoba, Specjalnosc i lek_spec po wykonaniu powyższego programu.

ID GABINET    NAZWISKO       
IMIE        TYP

----------    ------------   
--------    -------

128518        Kwiatkowski    
Andrzej     L

127518        Kowalski       
Jan         L

126519        Iksinski       
Paweł       L

125           Pacjętowski    
Arkadiusz   O

Tabela Osoba

ID     NAZWA

----   ----------

127    Kardiolog

126    Alergolog

125    Gastrolog

Tabela Specjalnosc

SPEC_ID        LEK_ID

--------       ------------

126            128

127            127

125            127

125            126

Tabela lek_spec

W obu przedstawionych rozwiązaniach hierarchie klas były
mapowane do jednej tabeli, a do rozpoznawania typu obiektu służyła specjalna
dodatkowa kolumna (dyskryminator). Oba rozwiązania nie mają praktycznie wpływu
na postać klas biznesowych (poza tym, że wymagają dodatkowego
identyfikatora). Hibernate wymaga jeszcze publicznych metod get i set
dla każdego atrybutu, który ma być mapowany. Istnienie takich metod nie jest
jednak w żaden sposób charakterystyczne dla zastosowania omawianego pakietu.
Ich definiowanie może być dobrą praktyką programistyczną. Warto nadmienić,
że niektóre środowiska programistyczne pozwalają na automatyczne
definiowanie metod setAtrybut, getAtrybut6. Omawiane rozwiązania
pozwalają na „oddalenie” się implementacji od JDBC. Stosując
Oracle TopLink nie ma potrzeby stosowania zapytań w języku SQL, co może
wydatnie uprościć tworzenie systemu informatycznego.

Literatura

[1] G. Reese Java Aplikacje bazodanowe, Najlepsze Rozwiązania
Helion 2003
[2] http://www.javaskyline.com/database.html
[3] Hibernate2 Reference Documentation, Version 2.0.2
[4] G. Lausen, G. Vossen Obiektowe bazy daych
WNT 2000
[5] C.J. Date Wprowadzenie do systemów baz danych WNT
2000
[6] N. Heudecker Introduction to Hibernate http://www.systemmobile.com/articles/IntroductionToHibernate.html
[7] S. W. Ambler Fundamentals of Mapping Objects to
Relational Databases
http://www.agiledate.org/essays/mappingObjects.html
[8] S. W. Ambler Mapping Objects to Relational Databases
http://www-106.ibm.com/developerworks/webservices/library/ws-mapping-to-rdb/
[8] H. Ong Oracle TopLinkBridging The
Object Relational
Gulf Aurora Consulting Pty
[9] K. Pepperdine (Middleware) Oracle9AS TopLink
By Example
Oracle Technology Network
[10] Dokumentacja techniczna Oracle TopLink
[11] Materiały na stronach serwisu www.oracle.com


1 Zakładamy dla uproszczenia, że atrybutowi odpowiada jedna kolumna tabeli.

2 Napisy italikiem należy zastąpić konkretnymi wartościami.

3 Chcąc przetestować przedstawiane rozwiązania (z użyciem
Hibernate) należy dopisać ręcznie, lub wygenerować brakujące metody get
i set. Powinny być one zdefiniowane dla każdego mapowanego atrybutu,
mieć publiczną widoczność i typ zgody z typem atrybutu.

4 Z dokumentu dla oszczędności miejsca usunięto nagłówek określający
typ dokumentu i definicje następujących atrybutów: Imie, PELSEL, NIP,
nazwisko, numerTelefonu i numerTelefonuKomorkowego. Chcąc przetestować rozwiązanie,
należy je dopisać zgodnie z poprzednio zaprezentowanym dokumentem.

5 Usunięto z niej dla oszczędności miejsca definicje importów i
komentarz, mówiący jakie narzędzie wygenerowało przedstawiany plik. Dokonano
w niej ręcznie niewielkich zmian.

6 Atrybut jest nazwą konkretnego atrybutu.