Skip to content

17. Aplikacja internetowa MVC w architekturze trójwarstwowej – Przykład 3 – System zarządzania bazami danych Firebird

17.1. Baza danych Firebird

W tej nowej wersji umieścimy listę osób w tabeli bazy danych Firebird. W dokumencie [http://tahe.developpez.com/divers/sql-firebird/] znajdują się informacje dotyczące instalacji i zarządzania tym SGBD. Poniższe zrzuty ekranu pochodzą z IBExpert, klienta do zarządzania bazami danych Interbase i Firebird.

Baza danych nosi nazwę [dbpersonnes.gdb]. Zawiera ona tabelę [PERSONNES]:

Image

Tabela o nazwie [PERSONNES] będzie zawierała listę osób zarządzanych przez aplikację internetową. Została ona utworzona za pomocą następujących poleceń SQL:

CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);


ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • wiersze 2–10: struktura tabeli [PERSONNES], przeznaczonej do przechowywania obiektów typu [Personne], odzwierciedla strukturę tego obiektu. Ponieważ typ logiczny nie istnieje w Firebirdzie, pole [MARIE] (wiersz 8) zostało zadeklarowane jako typ [SMALLINT], czyli liczba całkowita. Jego wartość będzie wynosić 0 (nieżonaty) lub 1 (żonaty).
  • wiersze 13–16: ograniczenia integralności odzwierciedlające ograniczenia walidatora danych [ValidatePersonne].
  • wiersz 19: pole ID jest kluczem głównym tabeli [PERSONNES]

Tabela [PERSONNES] może zawierać następujące dane:

Image

Baza danych [dbpersonnes.gdb] zawiera, oprócz tabeli [PERSONNES], obiekt zwany generatorem o nazwie [GEN_PERSONNES_ID]. Generator ten generuje kolejne liczby całkowite, które wykorzystamy do nadania wartości kluczu głównego [ID] klasy [PERSONNES]. Przyjrzyjmy się przykładowi ilustrującemu jego działanie:

Można zauważyć, że wartość generatora [GEN_PERSONNES_ID] uległa zmianie (dwukrotne kliknięcie na nim + F5 w celu odświeżenia):

 

Kolejność SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

pozwala zatem uzyskać następującą wartość generatora [GEN_PERSONNES_ID]. GEN_ID jest funkcją wewnętrzną Firebirda, a [RDB$DATABASE] – tabelą systemową tego SGBD.

17.2. Projekt Eclipse warstw [dao] i [service]

Aby opracować warstwy [dao] i [service] naszej aplikacji z bazą danych, wykorzystamy następujący projekt Eclipse o nazwie [mvc-personnes-03]:

Image

Projekt ten jest prostym projektem Java, a nie projektem internetowym opartym na Tomcat. Przypomnijmy, że wersja 2 naszej aplikacji będzie korzystać z warstwy [web] z wersji 1. Warstwa ta nie musi więc być od nowa tworzona.


Folder [src]


Ten folder zawiera kod źródłowy warstw [dao] i [service]:

Image

Znajdują się w nim różne pakiety:

  • [istia.st.mvc.personnes.dao]: zawiera warstwę [dao]
  • [istia.st.mvc.personnes.entites]: zawiera klasę [Personne]
  • [istia.st.mvc.personnes.service]: zawiera klasę [service]
  • [istia.st.mvc.personnes.tests]: zawiera testy JUnit dla warstw [dao] i [service]

a także pliki konfiguracyjne, które powinny znajdować się w katalogu ClassPath aplikacji.


Folder [database]


Ten folder zawiera bazę danych Firebird dotyczącą osób:

Image

  • [dbpersonnes.gdb] to baza danych.
  • [dbpersonnes.sql] to skrypt SQL służący do generowania bazy:
/******************************************************************************/
/***           Wygenerowano przez IBExpert 07.03.2006 27.04         .2006 10:27:11 ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

CREATE DATABASE 'C:\data\2005-2006\webjava\dvp-spring-mvc\mvc-38\database\DBPERSONNES.GDB'
USER 'SYSDBA' PASSWORD 'masterkey'
PAGE_SIZE 16384
DEFAULT CHARACTER SET NONE;



/******************************************************************************/
/***                                                               Generatory ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                                                   Tabele ***/
/******************************************************************************/



CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);

INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (1, 1, 'Major', 'Joachim', '1984-11-13', 1, 2);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (2, 1, 'Humbort', 'Mélanie', '1985-02-12', 0, 1);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (3, 1, 'Lemarchand', 'Charles', '1986-03-01', 0, 0);

COMMIT WORK;



/* Sprawdź definicje ograniczeń */

ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


/******************************************************************************/
/***                                                             Klucze główne ***/
/******************************************************************************/

ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);

Folder [lib]


Ten folder zawiera archiwa niezbędne do działania aplikacji:

Warto zwrócić uwagę na obecność pliku sterownika JDBC, [firebirdsql-full.jar] oraz SGBD dla Firebirda, a także pewnej liczby archiwów [spring-*.jar]. Mogliśmy skorzystać z jedynego archiwum o nazwie [spring.jar], które znajduje się w folderze [dist] dystrybucji i zawiera wszystkie klasy Springa. Można również użyć wyłącznie tych archiwów, które są niezbędne dla projektu. Tak właśnie postąpiliśmy w tym przypadku, kierując się komunikatami o brakujących klasach zgłaszanymi przez Eclipse oraz nazwami częściowych archiwów Springa. Wszystkie te archiwa z folderu [lib] zostały umieszczone w folderze Classpath projektu.


Folder [dist]


Ten folder będzie zawierał archiwa powstałe w wyniku kompilacji klas aplikacji:

Image

  • [personnes-dao.jar]: archiwum warstwy [dao]
  • [personnes-service.jar]: archiwum warstwy [service]

17.3. Warstwa [dao]

17.3.1. Składniki warstwy [dao]

Warstwa [dao] składa się z następujących klas i interfejsów:

Image

  • [IDao] to interfejs prezentowany przez warstwę [dao]
  • [DaoImplCommon] jest jej implementacją, w której grupa osób znajduje się w tabeli bazy danych. [DaoImplCommon] obejmuje funkcje niezależne od klasy SGBD.
  • [DaoImplFirebird] jest klasą pochodną od [DaoImplCommon], służącą do obsługi konkretnie bazy danych Firebird.
  • [DaoException] to typ niekontrolowanych wyjątków, generowanych przez warstwę [dao]. Klasa ta pochodzi z wersji 1.

Interfejs [IDao] ma następujący wygląd:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // lista wszystkich osób
    Collection getAll();
     // pobierz konkretną osobę
    Personne getOne(int id);
     // Dodaj/edytuj osobę
    void saveOne(Personne personne);
     // usunięcie osoby
    void deleteOne(int id);
}
  • Interfejs posiada te same cztery metody, co w poprzedniej wersji.

Klasa [DaoImplCommon] implementująca ten interfejs będzie wyglądać następująco:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // lista osób
    public Collection getAll() {
...
    }

     // wyświetlenie konkretnej osoby
    public Personne getOne(int id) {
...
    }

     // usunięcie osoby
    public void deleteOne(int id) {
...
    }

     // dodaj lub edytuj osobę
    public void saveOne(Personne personne) {
         // Czy parametr „osoba” jest prawidłowy?
        check(personne);
         // Dodanie czy zmiana?
        if (personne.getId() == -1) {
             // dodanie
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // dodaj osobę
    protected void insertPersonne(Personne personne) {
...
    }

     // edytuj osobę
    protected void updatePersonne(Personne personne) {
...
    }

     // sprawdzenie poprawności danych osoby
    private void check(Personne p) {
...
    }

...
}
  • wiersze 8–9: klasa [DaoImpl] implementuje interfejs [IDao], a tym samym cztery metody [getAll, getOne, saveOne, deleteOne].
  • wiersze 27–37: metoda [saveOne] wykorzystuje dwie metody wewnętrzne [insertPersonne] i [updatePersonne] w zależności od tego, czy ma zostać dodana, czy zmodyfikowana osoba.
  • wiersz 50: metoda prywatna [check] pochodzi z poprzedniej wersji. Nie będziemy do niej wracać.
  • wiersz 8: w celu zaimplementowania interfejsu [IDao] klasa [DaoImpl] wywodzi się z klasy Spring [SqlMapClientDaoSupport].

17.3.2. Warstwa dostępu do danych [iBATIS]

Klasa Spring [SqlMapClientDaoSupport] korzysta z zewnętrznego frameworka [Ibatis SqlMap] dostępnego pod adresem URL [http://ibatis.apache.org/]:

Image

[iBATIS] to projekt Apache, który ułatwia tworzenie warstw [dao] opartych na bazach danych. W przypadku [iBATIS] architektura warstwy dostępu do danych wygląda następująco:

[iBATIS] znajduje się pomiędzy warstwą aplikacji [dao] a sterownikiem bazy danych JDBC. Istnieją alternatywy dla [iBATIS], takie jak na przykład [Hibernate]:

Image

Korzystanie z frameworka [iBATIS] wymaga dwóch archiwów [ibatis-common, ibatis-sqlmap], które zostały umieszczone w folderze [lib] projektu:

Klasa [SqlMapClientDaoSupport] zawiera ogólną część kodu wykorzystującego framework [iBATIS] oraz c.a.d. fragmentów kodu, które występują we wszystkich warstwach [dao] wykorzystujących narzędzie [iBATIS]. Aby napisać nieogólną część kodu, czyli tę, która jest specyficzna dla warstwy [dao], wystarczy utworzyć klasę pochodną od klasy [SqlMapClientDaoSupport]. Właśnie to tutaj robimy.

Klasa [SqlMapClientDaoSupport] jest zdefiniowana w następujący sposób:

Image

Wśród metod tej klasy jedna z nich pozwala skonfigurować klienta [iBATIS], za pomocą którego będziemy korzystać z bazy danych:

Image

Obiekt [SqlMapClient sqlMapClient] to obiekt [IBATIS] służący do uzyskiwania dostępu do bazy danych. Sam w sobie implementuje on warstwę [iBATIS] naszej architektury:

Typowa sekwencja działań z wykorzystaniem tego obiektu wygląda następująco:

  1. zgłoszenie żądania połączenia do puli połączeń
  2. otwarcie transakcji
  3. wykonanie serii poleceń SQL zapisanych w pliku konfiguracyjnym
  4. zamknięcie transakcji
  5. zwrócenie połączenia do puli

Gdyby nasza implementacja [DaoImplCommon] współpracowała bezpośrednio z [iBATIS], musiałaby wielokrotnie wykonywać tę sekwencję. Jedynie operacja 3 jest specyficzna dla warstwy [dao], pozostałe operacje mają charakter ogólny. Klasa Spring [SqlMapClientDaoSupport] sama zajmie się operacjami 1, 2, 4 i 5, delegując operację 3 do swojej klasy pochodnej, w tym przypadku klasy [DaoImplCommon].

Aby mogła działać, klasa [SqlMapClientDaoSupport] potrzebuje odwołania do obiektu iBATIS [SqlMapClient sqlMapClient], który zapewni komunikację z bazą danych. Obiekt ten potrzebuje dwóch elementów do działania:

  • obiekt [DataSource] połączony z bazą danych, od którego będzie żądać połączeń
  • plik (lub pliki) konfiguracyjny, w którym zewnętrznie zapisane są polecenia SQL do wykonania. W rzeczywistości polecenia te nie znajdują się w kodzie Java. Są one identyfikowane za pomocą kodu w pliku konfiguracyjnym, a obiekt [SqlMapClient sqlMapClient] wykorzystuje ten kod do uruchomienia konkretnego polecenia SQL.

Wstępna konfiguracja naszej warstwy [dao], odzwierciedlająca powyższą architekturę, wyglądałaby następująco:


    <!-- klasy dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
</bean>

W tym miejscu inicjowana jest właściwość [sqlMapClient] (wiersz 3) klasy [DaoImplCommon] (wiersz 2). Odbywa się to za pomocą metody [setSqlMapClient] klasy [DaoImpl]. Ta klasa nie posiada tej metody. Posiada ją natomiast jej klasa nadrzędna [SqlMapClientDaoSupport]. To właśnie ona jest w rzeczywistości tutaj inicjowana.

Teraz w wierszu 4 odwołujemy się do obiektu o nazwie „sqlMapClient”, który pozostaje jeszcze do utworzenia. Jak już wspomniano, jest on typu [SqlMapClient], a typem [iBATIS] jest:

Image

[SqlMapClient] jest interfejsem. Spring udostępnia klasę [SqlMapClientFactoryBean], aby uzyskać obiekt implementujący ten interfejs:

Image

Przypomnijmy, że chcemy utworzyć instancję obiektu implementującego interfejs [SqlMapClient]. Najwyraźniej nie dotyczy to klasy [SqlMapClientFactoryBean]. Klasa ta implementuje interfejs [FactoryBean] (patrz wyżej). Posiada ona następującą metodę [getObject()]:

Image

Gdy zwracamy się do Springa o instancję obiektu implementującego interfejs [FactoryBean], Spring:

  • tworzy instancję klasy [I] – w tym przypadku tworzy instancję typu [SqlMapClientFactoryBean].
  • zwraca wywołującej metodzie wynik metody [I].getObject() – metoda [SqlMapClientFactoryBean].Metoda getObject() zwróci tutaj obiekt implementujący interfejs [SqlMapClient].

Aby zwrócić obiekt implementujący interfejs [SqlMapClient], klasa [SqlMapClientFactoryBean] potrzebuje dwóch informacji niezbędnych dla tego obiektu:

  • obiekt [DataSource] połączony z bazą danych, od którego będzie żądać połączeń
  • plik (lub pliki) konfiguracyjny, w którym zewnętrznie zdefiniowano polecenia SQL do wykonania

Klasa [SqlMapClientFactoryBean] posiada metody set służące do inicjalizacji tych dwóch właściwości:

Image

Robimy postępy... Nasz plik konfiguracyjny nabiera konkretnych kształtów i wygląda następująco:


<!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasy dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
  • wiersze 2–3: bean „sqlMapClient” jest typu [SqlMapClientFactoryBean]. Z powyższego wyjaśnienia wynika, że gdy zwracamy się do Springa o instancję tego beana, otrzymujemy obiekt implementujący interfejs iBATIS [SqlMapClient]. To właśnie ten ostatni obiekt zostanie zatem uzyskany w wierszu 14.
  • wiersze 7–9: określamy, że plik konfiguracyjny niezbędny dla obiektu iBATIS [SqlMapClient] nosi nazwę „sql-map-config-firebird.xml” i należy go wyszukać w katalogu ClassPath aplikacji. W tym miejscu wykorzystywana jest metoda [SqlMapClientFactoryBean].setConfigLocation.
  • Wiersze 4–6: inicjujemy właściwość [dataSource] obiektu [SqlMapClientFactoryBean] za pomocą jego metody [setDataSource].

W wierszu 5 odwołujemy się do komponentu o nazwie „dataSource”, który pozostaje jeszcze do utworzenia. Jeśli przyjrzymy się parametrowi oczekiwanemu przez metodę [setDataSource] obiektu [SqlMapClientFactoryBean], widzimy, że jest on typu [DataSource]:

Image

Ponownie mamy do czynienia z interfejsem, dla którego musimy znaleźć klasę implementacyjną. Rolą takiej klasy jest zapewnienie aplikacji wydajnych połączeń z określoną bazą danych. SGBD nie może utrzymywać jednocześnie dużej liczby otwartych połączeń. Aby zmniejszyć liczbę połączeń otwartych w danym momencie, przy każdej interakcji z bazą danych musimy:

  • otworzyć połączenie
  • rozpocząć transakcję
  • wysłać polecenia SQL
  • zamknąć transakcję
  • zamknąć połączenie

Ciągłe otwieranie i zamykanie połączeń jest czasochłonne. Aby rozwiązać te dwa problemy (ograniczyć zarówno liczbę połączeń otwartych w danym momencie, jak i koszt ich otwierania/zamykania), klasy implementujące interfejs [DataSource] często postępują w następujący sposób:

  • zaraz po instancjonowaniu otwierają N połączeń z docelową bazą danych. N ma zazwyczaj wartość domyślną i najczęściej można ją zdefiniować w pliku konfiguracyjnym. Te N połączeń pozostają otwarte przez cały czas i tworzą pulę połączeń dostępnych dla wątków aplikacji.
  • gdy wątek aplikacji zażąda otwarcia połączenia, obiekt [DataSource] przydziela mu jedno z N połączeń otwartych podczas uruchomienia, o ile są one nadal dostępne. Gdy aplikacja zamyka połączenie, w rzeczywistości nie jest ono zamykane, lecz po prostu przywracane do puli dostępnych połączeń.

Istnieje wiele swobodnie dostępnych implementacji interfejsu [DataSource]. W tym przypadku wykorzystamy implementację [commons DBCP] dostępną pod adresem URL [http://jakarta.apache.org/commons/dbcp/]:

Image

Korzystanie z narzędzia [commons DBCP] wymaga dwóch archiwów [commons-dbcp, commons-pool], które zostały umieszczone w folderze [lib] projektu:

Klasa [BasicDataSource] z pliku [commons DBCP] zapewnia implementację [DataSource], której potrzebujemy:

Image

Klasa ta zapewni nam pulę połączeń umożliwiających dostęp do bazy danych Firebird [dbpersonnes.gdb] naszej aplikacji. W tym celu należy podać jej informacje niezbędne do utworzenia połączeń w puli:

  1. nazwę sterownika JDBC, który ma być używany – zainicjowanego za pomocą [setDriverClassName]
  2. nazwę adresu URL bazy danych, z której ma korzystać – zainicjowaną jako [setUrl]
  3. identyfikator użytkownika będącego właścicielem połączenia – domyślnie ustawiony na [setUsername] (a nie setUserName, jak można by się spodziewać)
  4. jego hasło – domyślnie ustawione na [setPassword]

Plik konfiguracyjny naszej warstwy [dao] może wyglądać następująco:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- źródło danych DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- uwaga: nie należy pozostawiać spacji między dwoma tagami <value> w adresie URL -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasa dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • wiersze 7–9: nazwa sterownika JDBC dla Firebirda SGBD
  • wiersze 11–13: adres URL bazy danych Firebird [dbpersonnes.gdb]. Należy zwrócić szczególną uwagę na jego zapis. Pomiędzy tagami <value> a adresem URL nie może być żadnych spacji.
  • wiersze 14–16: właściciel połączenia – w tym przypadku [sysdba], który jest domyślnym administratorem dystrybucji Firebird
  • wiersze 17–19: jego hasło [masterkey] – również wartość domyślna

Zrobiliśmy spory postęp, ale wciąż pozostają pewne kwestie konfiguracyjne do wyjaśnienia: wiersz 28 odwołuje się do pliku [sql-map-config-firebird.xml], który powinien skonfigurować klienta [SqlMapClient] dla iBATIS. Zanim przeanalizujemy jego zawartość, pokażmy lokalizację tych plików konfiguracyjnych w naszym projekcie Eclipse:

Image

  • [spring-config-test-dao-firebird.xml] to plik konfiguracyjny warstwy [dao], którą właśnie przeanalizowaliśmy
  • Plik [sql-map-config-firebird.xml] jest odwołany przez plik [spring-config-test-dao-firebird.xml]. Przyjrzyjmy się mu.
  • Plik [personnes-firebird.xml] jest odwołany przez plik [sql-map-config-firebird.xml]. Przyjrzyjmy się mu.

Trzy powyższe pliki znajdują się w folderze [src]. W środowisku Eclipse oznacza to, że podczas wykonywania będą one znajdować się w folderze [bin] projektu (nie pokazanym powyżej). Folder ten jest częścią pliku ClassPath aplikacji. Ostatecznie powyższe trzy pliki będą więc rzeczywiście znajdować się w folderze ClassPath aplikacji. Jest to konieczne.

Plik [sql-map-config-firebird.xml] ma następującą zawartość:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
  • plik ten musi zawierać tag główny <sqlMapConfig> (wiersze 6 i 8)
  • wiersz 7: znacznik <sqlMap> służy do wskazania plików zawierających polecenia SQL, które mają zostać wykonane. Często, choć nie jest to obowiązkowe, na każdą tabelę przypada jeden plik. Pozwala to zgromadzić polecenia SQL dotyczące danej tabeli w jednym pliku. Często jednak spotyka się polecenia SQL obejmujące kilka tabel. W takim przypadku powyższy podział nie ma zastosowania. Należy po prostu pamiętać, że wszystkie pliki oznaczone tagami <sqlMap> zostaną połączone. Pliki te są wyszukiwane w pliku ClassPath aplikacji.

Plik [personnes-firebird.xml] opisuje polecenia SQL, które zostaną wysłane do tabeli [PERSONNES] w bazie danych Firebird [dbpersonnes.gdb]. Jego zawartość jest następująca:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- alias klasy [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabela mapowania [PERSONNES] – obiekt [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- lista wszystkich osób -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- pobranie konkretnej osoby -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#wartość#</select>
    <!-- dodaj osobę -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        <selectKey keyProperty="id">
            SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
        </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #wersja#, #nazwisko#, #imię#, #dateNaissance#, #mąż#, 
        #nbEnfants#) </insert>
    <!-- zaktualizuj dane osoby -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#wersja#+1, NOM=#nazwisko#, PRENOM=#imię#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#mąż#, NBENFANTS=#nbEnfants# WHERE ID=#id# oraz 
        VERSION=#wersja#</update>
    <!-- usuń osobę -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#wartość# </delete>
</sqlMap>
  • plik musi zawierać tag główny <sqlMap> (wiersze 7 i 45)
  • wiersze 9–10: aby ułatwić tworzenie pliku, klasie [istia.st.springmvc.personnes.entites.Personne] nadano alias (synonim) [Personne.classe].
  • wiersze 12–21: ustalają powiązania między kolumnami tabeli [PERSONNES] a polami obiektu [Personne].
  • wiersze 23–24: zapytanie SQL [select] w celu uzyskania wszystkich osób z tabeli [PERSONNES]
  • wiersze 26–27: polecenie SQL [select] w celu pobrania konkretnej osoby z tabeli [PERSONNES]
  • wiersze 29–36: polecenie SQL [insert], które wstawia osobę do tabeli [PERSONNES]
  • wiersze 38–41: polecenie SQL [update], które aktualizuje osobę z tabeli [PERSONNES]
  • wiersze 42–44: polecenie SQL [delete], które usuwa osobę z tabeli [PERSONNES]

Rola i znaczenie zawartości pliku [personnes-firebird.xml] zostaną wyjaśnione na podstawie analizy klasy [DaoImplCommon], która implementuje warstwę [dao].

17.3.3. Klasa [DaoImplCommon]

Wróćmy do architektury dostępu do danych:

Klasa [DaoImplCommon] ma następujący wygląd:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // lista osób
    public Collection getAll() {
...
    }

     // pobranie konkretnej osoby
    public Personne getOne(int id) {
...
    }

     // usunięcie osoby
    public void deleteOne(int id) {
...
    }

     // dodaj lub zmień osobę
    public void saveOne(Personne personne) {
         // czy parametr osoby jest prawidłowy?
        check(personne);
         // dodanie czy zmiana?
        if (personne.getId() == -1) {
             // dodanie
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // dodaj osobę
    protected void insertPersonne(Personne personne) {
...
    }

     // edytuj osobę
    protected void updatePersonne(Personne personne) {
...
    }

     // sprawdzenie poprawności danych osoby
    private void check(Personne p) {
...
    }

...
}

Przeanalizujemy te metody po kolei.


getAll


Ta metoda pozwala uzyskać wszystkie osoby z listy. Jej kod jest następujący:

1
2
3
4
     // lista osób
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Przypomnijmy sobie najpierw, że klasa [DaoImplCommon] wywodzi się z klasy Spring [SqlMapClientDaoSupport]. To właśnie ta klasa posiada metodę [getSqlMapClientTemplate()], wykorzystaną w wierszu 3 powyżej. Metoda ta ma następującą sygnaturę:

Image

Typ [SqlMapClientTemplate] enkapsuluje obiekt [SqlMapClient] z warstwy [iBATIS]. To właśnie za jego pośrednictwem uzyskamy dostęp do bazy danych. Typ [iBATIS] SqlMapClient mógłby być używany bezpośrednio, ponieważ klasa [SqlMapClientDaoSupport] ma do niego dostęp:

Image

Wadą klasy [iBATIS] SqlMapClient jest to, że generuje ona wyjątki typu [SQLException], który jest typem wyjątku kontrolowanego, c.a.d. które muszą być obsługiwane za pomocą bloku try/catch lub zadeklarowane w sygnaturach metod, które je wywołują. Należy jednak pamiętać, że warstwa [dao] implementuje interfejs [IDao], którego metody nie zawierają wyjątków w swoich sygnaturach. Metody klas implementujących interfejs [IDao] również nie mogą zatem zawierać wyjątków w swoich sygnaturach. Musimy zatem przechwycić każdy wyjątek typu [SQLException] rzucany przez warstwę [iBATIS] i zamknąć go w wyjątku niekontrolowanym. Typ [DaoException] z naszego projektu nadawałby się do tego zamknięcia.

Zamiast samodzielnie obsługiwać te wyjątki, powierzymy je typowi Spring [SqlMapClientTemplate], który hermetyzuje obiekt [SqlMapClient] z warstwy [iBATIS]. W rzeczywistości typ [SqlMapClientTemplate] został stworzony w celu przechwytywania wyjątków typu [SQLException] rzucanych przez warstwę [SqlMapClient] i enkapsulowania ich w niekontrolowanym typie [DataAccessException] . Takie zachowanie nam odpowiada. Należy jedynie pamiętać, że warstwa [dao] może teraz generować dwa rodzaje niekontrolowanych wyjątków:

  • nasz własny typ [DaoException]
  • typ Springa [DataAccessException]

Typ [SqlMapClientTemplate] jest zdefiniowany w następujący sposób:

Image

Implementuje on następujący interfejs [SqlMapClientOperations]:

Image

Interfejs ten definiuje metody umożliwiające wykorzystanie zawartości pliku [personnes-firebird.xml]:

[queryForList]

Image

Ta metoda umożliwia wysłanie zlecenia [SELECT] i pobranie jego wyniku w postaci listy obiektów:

  • [statementName]: identyfikator (id) zlecenia [select] w pliku konfiguracyjnym
  • [parameterObject]: obiekt „parametr” dla skonfigurowanego zlecenia [select]. Obiekt „parametr” może przybierać dwie formy:
    • obiekt zgodny ze standardem JavaBean: parametry zlecenia [select] są wówczas nazwami pól obiektu JavaBean. Podczas wykonywania zlecenia [select] są one zastępowane wartościami tych pól.
    • słownik: parametry zlecenia [select] są wówczas kluczami słownika. Podczas wykonywania zlecenia [select] klucze te są zastępowane wartościami przypisanymi im w słowniku.
  • jeśli zapytanie [SELECT] nie zwraca żadnego wiersza, wynik [List] jest obiektem pustym, ale nie dotyczy to null (do sprawdzenia).

[queryForObject]

Image

Metoda ta jest zasadniczo identyczna z poprzednią, ale zwraca tylko jeden obiekt. Jeśli [SELECT] nie zwraca żadnego wiersza, wynikiem jest wskaźnik null.

[insert]

Image

Metoda ta pozwala na wykonanie polecenia SQL [insert] skonfigurowanego za pomocą drugiego parametru. Zwracanym obiektem jest klucz główny wstawionego wiersza. Nie ma obowiązku korzystania z tego wyniku.

[update]

Image

Metoda ta pozwala na wykonanie polecenia SQL [update], którego parametry są określone przez drugi parametr. Wynikiem jest liczba wierszy zmodyfikowanych przez polecenie SQL [update].

[delete]

Image

Metoda ta pozwala na wykonanie polecenia SQL [delete] skonfigurowanego za pomocą drugiego parametru. Wynikiem jest liczba wierszy usuniętych przez polecenie SQL [delete].

Wróćmy do metody [getAll] klasy [DaoImplCommon]:

1
2
3
4
     // lista osób
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • wiersz 4: wykonywane jest polecenie [select] o nazwie „Personne.getAll”. Nie ma ono parametrów, więc obiektem „parametr” jest null.

W [personnes-firebird.xml] polecenie [select] o nazwie „Personne.getAll” ma następującą treść:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- alias klasy [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabela mapowania [PERSONNES] – obiekt [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- lista wszystkich osób -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • wiersz 23: polecenie SQL „Personne.getAll” nie ma parametrów (brak parametrów w treści zapytania).
  • Wiersz 3 metody [getAll] wymaga wykonania zapytania [select] o nazwie „Personne.getAll”. Zapytanie to zostanie wykonane. [iBATIS] opiera się na JDBC. Wiemy zatem, że wynik zapytania zostanie uzyskany w postaci obiektu [ResultSet]. W wierszu 23 atrybut [resultMap] tagu <select> wskazuje [iBATIS], jakiego „resultMap ” ma użyć do przekształcenia każdego wiersza obiektu [ResultSet] w obiekt. Jest to „resultMap” – [Personne.map] zdefiniowany w wierszach 12–21, który określa, w jaki sposób przejść od wiersza tabeli [PERSONNES] do obiektu typu [Personne]. [iBATIS] wykorzysta te powiązania, aby dostarczyć listę obiektów typu [Personne] na podstawie wierszy obiektu [ResultSet].
  • Wiersz 3 metody [getAll] zwraca następnie zbiór obiektów typu [Personne]
  • metoda [queryForList] może wygenerować wyjątek Spring [DataAccessException]. Pozwalamy, aby został on przekazany dalej.

Pozostałe metody klasy [AbstractDaoImpl] omówimy pokrótce, ponieważ najważniejsze informacje dotyczące korzystania z metody [iBATIS] zostały już przedstawione podczas analizy metody [getAll].


getOne


Ta metoda pozwala uzyskać dane osoby identyfikowanej na podstawie jej kodu [id]. Jej kod jest następujący:

         // pobranie konkretnej osoby
    public Personne getOne(int id) {
         // pobieramy  z BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
         // czy coś udało się pobrać?
        if (personne == null) {
             // wywołujemy wyjątek
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
         // zwracamy  osobę
        return personne;
    }
  • wiersz 4: żąda wykonania polecenia [select] o nazwie „Personne.getOne”. W pliku [personnes-firebird.xml] wygląda ono następująco:

<!-- pobieramy konkretną osobę -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#wartość#</select>

Zlecenie SQL jest konfigurowane za pomocą parametru #value# (wiersz 4). Atrybut #value# określa wartość parametru przekazanego do polecenia SQL, gdy parametr ten jest typu prostego: Integer, Double, String itp. W atrybutach tagu <select> atrybut [parameterClass] wskazuje, że parametr jest typu całkowitego (wiersz 2). W wierszu 5 [getOne] widać, że parametr ten jest identyfikatorem poszukiwanej osoby w postaci obiektu Integer. Ta zmiana typu jest obowiązkowa, ponieważ drugi parametr [queryForList] musi być typu [Object].

Wynik zapytania [select] należy przekształcić w obiekt za pomocą atrybutu [resultMap="Personne.map"] (wiersz 2). W ten sposób otrzymamy typ [Personne].

  • wiersze 7–11: jeśli zapytanie o numer [select] nie zwróciło żadnego wiersza, pobieramy wówczas wskaźnik o numerze null z wiersza 4. Oznacza to, że nie znaleziono poszukiwanej osoby. W takim przypadku uruchamia się zapytanie o kodzie 2: [DaoException] (wiersze 9–10).
  • wiersz 13: jeśli nie wystąpił żaden wyjątek, zwracany jest żądany obiekt [Personne].

deleteOne


Metoda ta pozwala na usunięcie osoby zidentyfikowanej na podstawie jej identyfikatora [id]. Jej kod jest następujący:

     // usunięcie osoby
    public void deleteOne(int id) {
         // usuwamy osobę
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
         // czy operacja się powiodła
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • wiersze 4–5: żądają wykonania polecenia o nazwie „Personne.deleteOne” o numerze [delete]. W pliku [personnes-firebird.xml] wygląda ono następująco:

<!-- usunięcie osoby -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#wartość# </delete>

Zlecenie SQL jest konfigurowane za pomocą parametru #value# (wiersz 3) typu [parameterClass="int"] (wiersz 2). Będzie to identyfikator poszukiwanej osoby (wiersz 5 w pliku deleteOne)

  • wiersz 4: wynikiem metody [SqlMapClientTemplate].delete jest liczba usuniętych wierszy.
  • wiersze 7–8: jeśli zapytanie [delete] nie usunęło żadnego wiersza, oznacza to, że dana osoba nie istnieje. Uruchamia się zapytanie [DaoException] o kodzie 2 (wiersz 8).

saveOne


Metoda ta pozwala na dodanie nowej osoby lub zmianę danych istniejącej osoby. Jej kod jest następujący:

         // dodaj lub zmień osobę
    public void saveOne(Personne personne) {
         // czy parametr osoba jest prawidłowy?
        check(personne);
         // dodanie czy zmiana?
        if (personne.getId() == -1) {
             // dodanie
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • wiersz 4: sprawdzamy poprawność osoby za pomocą metody [check]. Metoda ta istniała już w poprzedniej wersji i została wówczas skomentowana. Wywołuje ona [DaoException], jeśli osoba jest nieprawidłowa. Pozwalamy, aby ta metoda została wywołana.
  • wiersz 6: jeśli dotarliśmy do tego miejsca, oznacza to, że nie wystąpił żaden wyjątek. Osoba jest zatem prawidłowa.
  • wiersze 6–11: w zależności od identyfikatora osoby mamy do czynienia z dodaniem (id = -1) lub aktualizacją (id ≠ -1). W obu przypadkach wywoływane są dwie metody wewnętrzne klasy:
    • insertPersonne: do dodania
    • updatePersonne: do aktualizacji

insertPersonne


Ta metoda pozwala na dodanie nowej osoby. Jej kod wygląda następująco:

// dodaj osobę
    protected void insertPersonne(Personne personne) {
         // Pierwsza wersja
        personne.setVersion(1);
         // oczekujemy 10 ms  w celach testowych należy ustawić wartość „true” zamiast „false”
        if (true)
            wait(10);
         // wstawiamy nową osobę do tabeli BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • wiersz 4: ustawiamy na 1 numer wersji osoby, którą właśnie tworzymy
  • wiersz 9: dokonuje się wstawienia za pomocą zapytania o nazwie „Personne.insertOne”, które ma następującą postać:

        <insert id="Personne.insertOne" parameterClass="Personne.classe">
            <selectKey keyProperty="id">
                SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
            </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #wersja#, #nazwisko#, #imię#, #dateNaissance#, #mąż#, 
    #nbEnfants#) </insert>

Jest to zapytanie z parametrami, a parametr ma typ [Personne] (parameterClass = „Personne.classe”, wiersz 1). Pola obiektu [Personne] przekazane jako parametr (wiersz 9 w insertPersonne) służą do wypełnienia kolumn wiersza, który ma zostać wstawiony do tabeli [PERSONNES] (wiersze 5–8). Mamy problem do rozwiązania. Podczas wstawiania obiekt [Personne], który ma zostać wstawiony, ma identyfikator równy -1. Należy zastąpić tę wartość prawidłowym kluczem głównym. W tym celu wykorzystujemy wiersze 2–4 powyższego tagu <selectKey>. Wskazują one:

  • (ciąg dalszy)
    • zapytanie SQL, które należy wykonać w celu uzyskania wartości klucza głównego. Podane tutaj zapytanie jest tym samym, które przedstawiliśmy w paragrafie 17.1. Należy zwrócić uwagę na dwie kwestie:
      • as „value” jest obowiązkowe. Można również napisać as value, ale value jest słowem kluczowym w Firebirdzie, które musiało zostać ujęte w cudzysłowy.
      • Tabela Firebird nosi w rzeczywistości nazwę [RDB$DATABASE]. Jednak znak $ jest interpretowany jako [iBATIS]. Zabezpieczono to poprzez podwojenie znaku.
    • Pole obiektu [Personne], które należy zainicjować wartością pobraną przez polecenie [SELECT], w tym przypadku jest to pole [id]. To atrybut [keyProperty] w wierszu 2 wskazuje to pole.
  • wiersze 6–7: na potrzeby testów będziemy musieli odczekać 10 ms przed dokonaniem wstawienia, aby sprawdzić, czy występują konflikty między wątkami, które próbowałyby jednocześnie dokonywać dodawania danych.

updatePersonne


Ta metoda pozwala na modyfikację osoby już istniejącej w tabeli [PERSONNES]. Jej kod jest następujący:

// edytuj osobę
    protected void updatePersonne(Personne personne) {
         // oczekujemy 10 ms  w celach testowych należy ustawić wartość true zamiast false
        if (true)
            wait(10);
         // zmiana
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Aktualizacja może się nie powieść z co najmniej dwóch powodów:
    1. osoba, którą należy zaktualizować, nie istnieje
    2. osoba, którą należy zaktualizować, istnieje, ale wątek próbujący ją zmodyfikować nie posiada właściwej wersji
  • wiersze 7–8: wykonywane jest zapytanie o nazwie „Personne.updateOne” o kodzie SQL [update]. Brzmi ono następująco:

    <!-- aktualizacja danych osoby -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#wersja#+1, NOM=#nazwisko#, PRENOM=#imię#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#mąż#, NBENFANTS=#nbEnfants# WHERE ID=#id# oraz 
VERSION=#wersja#</update>
  • (ciąg dalszy)
    • wiersz 2: zapytanie jest skonfigurowane i przyjmuje jako parametr typ [Personne] (parameterClass = „Personne.classe”). Jest to osoba, którą należy zmodyfikować (wiersz 8 – updatePersonne).
    • Chcemy zmodyfikować wyłącznie osobę z tabeli [PERSONNES], która ma ten sam numer [id] i tę samą wersję [version] co parametr. Dlatego wprowadzono ograniczenie [WHERE ID=#id# and VERSION=#version#]. Jeśli taka osoba zostanie znaleziona, jest ona aktualizowana zgodnie z danymi z parametru, a jej wersja jest zwiększana o 1 (wiersz 3 powyżej).
  • wiersz 9: pobierana jest liczba zaktualizowanych wierszy.
  • wiersze 10–11: jeśli liczba ta wynosi zero, uruchamiany jest kod [DaoException] o wartości 2, wskazujący, że albo osoba, którą należy zaktualizować, nie istnieje, albo w międzyczasie zmieniła wersję.

17.4. Testy warstwy [dao]

17.4.1. Testy implementacji [DaoImplCommon]

Teraz, gdy napisaliśmy warstwę [dao], zamierzamy ją przetestować za pomocą testów JUnit:

Image

Zanim przystąpimy do intensywnych testów, możemy zacząć od prostego programu typu [main], który wyświetli zawartość tabeli [PERSONNES]. Jest to klasa [MainTestDaoFirebird]:

package istia.st.mvc.personnes.tests;

import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;
import java.util.Iterator;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class MainTestDaoFirebird {
    public static void main(String[] args) {
        IDao dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
         // aktualna lista
        Collection personnes = dao.getAll();
         // wyświetlanie konsoli
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

Plik konfiguracyjny [spring-config-test-dao-firebird.xml] warstwy [dao], wykorzystywany w wierszach 13–14, ma następującą treść:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- źródło danych DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- uwaga: nie pozostawiaj spacji między dwoma tagami <value> -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasa dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Plik ten został omówiony w punkcie 17.3.2.

W celu przeprowadzenia testu uruchomiono Firebirda o numerze SGBD. Zawartość tabeli [PERSONNES] jest następująca:

Image

Uruchomienie programu [MainTestDaoFirebird] daje następujące wyniki na ekranie:

Image

Udało się uzyskać listę osób. Można przejść do testu JUnit.

Test JUnit [TestDaoFirebird] wygląda następująco:

package istia.st.mvc.personnes.tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;
import junit.framework.TestCase;

public class TestDaoFirebird extends TestCase {

     // warstwa [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // producent
    public void setUp() {
        dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
    }

     // lista osób
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modyfikacja – usunięcie nieistniejącego elementu
    public void test2() throws ParseException {
..
    }

     // zarządzanie wersjami osób
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blokowanie optymistyczne – dostęp wielowątkowy
    public void test4() throws Exception {
...
    }

     // testy poprawności saveOne
    public void test5() throws ParseException {
....
    }

     // wstawianie wielowątkowe
    public void test6() throws ParseException, InterruptedException{
...
}
  • Testy od [test1] do [test5] są takie same jak w wersji 1, z wyjątkiem testu [test4], który uległ niewielkiej zmianie. Test [test6] jest natomiast nowy. Omówimy tylko te dwa testy.

[test4]


[test4] ma na celu przetestowanie metody [updatePersonne - DaoImplCommon]. Przypominamy jej kod:

// modyfikacja osoby
    protected void updatePersonne(Personne personne) {
         // oczekiwanie 10 ms  w testach należy ustawić wartość true zamiast false
        if (true)
            wait(10);
         // modyfikacja
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • wiersze 4–5: oczekujemy 10 ms. W ten sposób zmuszamy wątek wykonujący [updatePersonne] do utraty dostępu do procesora, co może zwiększyć nasze szanse na zaobserwowanie konfliktów dostępu między konkurującymi wątkami.

[test4] uruchamia N=100 wątków, których zadaniem jest jednoczesne zwiększenie o 1 liczby dzieci tej samej osoby. Chcemy sprawdzić, w jaki sposób obsługiwane są konflikty wersji i konflikty dostępu.

    public void test4() throws Exception {
         // dodanie osoby
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // utworzenie N wątków aktualizujących liczbę dzieci
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // czekamy na zakończenie działania wątków
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // pobieranie osoby
        p1 = dao.getOne(id1);
         // osoba ta musi mieć N dzieci
        assertEquals(N, p1.getNbEnfants());
         // usunięcie osoby p1
        dao.deleteOne(p1.getId());
         // weryfikacja
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // powinien wystąpić błąd o kodzie 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

Wątki są tworzone w wierszach 8–13. Każdy z nich zwiększy o 1 liczbę dzieci osoby utworzonej w wierszach 3–5. Wątki aktualizacyjne [ThreadDaoMajEnfants ] są następujące:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nazwa wątku
    private String name;

     // odwołanie do warstwy [dao]
    private IDao dao;

     // identyfikator osoby, nad którą będziemy pracować
    private int idPersonne;

     // twórca
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // rdzeń wątku
    public void run() {
         // śledzenie
        suivi("lancé");
         // pętla trwa, dopóki nie uda się zwiększyć wartości o 1
         // liczba dzieci osoby idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // pobieramy kopię osoby z idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // kontynuacja
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
             // oczekiwanie 10 ms przed zwolnieniem procesora
            try {
                 // kontynuacja
                suivi("début attente");
                 // przerywamy działanie, aby zwolnić procesor
                Thread.sleep(10);
                 // kontynuacja
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // oczekiwanie zakończone – próba zatwierdzenia kopii
             // w międzyczasie inne wątki mogły zmodyfikować oryginał
            int codeErreur = 0;
            try {
                 // zwiększamy o 1 liczbę potomków tej kopii
                personne.setNbEnfants(nbEnfants + 1);
                 // próbujemy zmodyfikować oryginał
                dao.saveOne(personne);
                 // operacja się powiodła – oryginał został zmodyfikowany
                fini = true;
            } catch (DaoException ex) {
                 // pobieramy kod błędu
                codeErreur = ex.getCode();
                 // jeśli wystąpił błąd ID lub błąd o numerze 2, ponawiamy próbę aktualizacji
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // nieobsługiwany wyjątek – pozwalamy na jego zgłoszenie
                    throw ex;
                }
            }
        }
         // monitorowanie
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // monitorowanie
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}

Aktualizacja osoby może się nie powieść, ponieważ osoba, którą chcemy zmodyfikować, nie istnieje lub została wcześniej zaktualizowana przez inny wątek. Oba te przypadki są tutaj obsługiwane w wierszach 67–69. W obu tych przypadkach metoda [updatePersonne] uruchamia wątek [DaoException] o kodzie 2. Wątek zostanie wówczas skierowany do ponownego rozpoczęcia procedury aktualizacji od początku (pętla while, wiersz 34).


[test6]


Metoda [test6] ma na celu przetestowanie metody [insertPersonne - DaoImplCommon]. Przypomnijmy jej kod:

// dodaj osobę
    protected void insertPersonne(Personne personne) {
         // pierwsza wersja
        personne.setVersion(1);
         // czekamy 10 ms  na potrzeby testów należy ustawić wartość „true” zamiast „false”
        if (true)
            wait(10);
         // wstawiamy nową osobę do tabeli BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • wiersze 6–7: czekamy 10 ms, aby wymusić utratę dostępu do procesora przez wątek wykonujący [insertPersonne], zwiększając w ten sposób prawdopodobieństwo wystąpienia konfliktów spowodowanych przez wątki dokonujące wstawień w tym samym czasie.

Kod [test6] wygląda następująco:

     // wstawianie wielowątkowe
    public void test6() throws ParseException, InterruptedException{
         // utworzenie osoby
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
         // którą powielamy N razy w tablicy
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
         // utworzenie N wątków wstawiania – każdy wątek wstawia 1 osobę
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
         // czekamy na zakończenie działania wątków
        for (int i = 0; i < taches.length; i++) {
             // wątek nr i
            taches[i].join();
             // usunięcie osoby
            dao.deleteOne(personnes[i].getId());
        }
}

Tworzymy 100 wątków, które będą jednocześnie wstawiać 100 różnych osób. Wszystkie te 100 wątków otrzyma klucz główny dla osoby, którą mają wstawić, a następnie zostanie przerwanych na 10 ms (wiersz 10 – insertPersonne), zanim będą mogły dokonać wstawienia. Chcemy sprawdzić, czy wszystko przebiega prawidłowo, a w szczególności, czy wszystkie wątki otrzymują różne wartości klucza głównego.

  • wiersze 7–11: tworzona jest tablica zawierająca 100 osób. Wszystkie te osoby są kopiami osoby p utworzonej w wierszach 4–5.
  • wiersze 14–17: uruchamianych jest 100 wątków wstawiania. Każdy z nich ma za zadanie wstawić jedną ze 100 utworzonych wcześniej osób.
  • wiersze 19–23: wątek [test6] czeka na zakończenie każdego ze 100 uruchomionych przez siebie wątków. Po wykryciu zakończenia wątku nr i usuwa osobę, którą ten wątek właśnie wstawił.

Wątek dodający [ThreadDaoInsertPersonne] wygląda następująco:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoInsertPersonne extends Thread {
     // nazwa wątku
    private String name;

     // odniesienie do warstwy [dao]
    private IDao dao;

     // identyfikator osoby, nad którą będziemy pracować
    private Personne personne;

     // twórca
    public ThreadDaoInsertPersonne(String name, IDao dao, Personne personne) {
        this.name = name;
        this.dao = dao;
        this.personne = personne;
    }

     // rdzeń wątku
    public void run() {
         // śledzenie
        suivi("lancé");
         // wstawienie
        dao.saveOne(personne);
         // śledzenie
        suivi("a terminé");
    }

     // śledzenie
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • wiersze 19–22: konstruktor wątku zapamiętuje osobę, którą ma wstawić, oraz warstwę [dao], której ma użyć do wykonania tego wstawienia.
  • wiersz 30: osoba zostaje wstawiona. Jeśli wystąpi wyjątek, jest on przekazywany do [test6].

Testy


Podczas testów uzyskano następujące wyniki:

Test [test4] zakończył się zatem niepowodzeniem. Liczba dzieci wzrosła do 69 zamiast oczekiwanych 100. Co się stało? Przyjrzyjmy się logom ekranowym. Wskazują one na występowanie wyjątków zgłoszonych przez Firebird:


Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];   
--– Błąd wystąpił w personnes-firebird.xml.  
--– Błąd wystąpił podczas stosowania mapy parametrów.  
--– Sprawdź plik Personne.updateOne-InlineParameterMap.  
--– Sprawdź instrukcję (aktualizacja nie powiodła się).  
--– Przyczyna: org.firebirdsql.jdbc.FBSQLException: Wyjątek GDS. 335544336. zakleszczenie
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--— Błąd wystąpił w personnes-firebird.xml.  
--– Błąd wystąpił podczas stosowania mapy parametrów.  
  • wiersz 1 – wystąpił wyjątek Spring o nazwie [org.springframework.jdbc.UncategorizedSQLException]. Jest to wyjątek niekontrolowany, który posłużył do enkapsulacji wyjątku wygenerowanego przez sterownik Firebirda o nazwie JDBC, opisanego w wierszu 6.
  • wiersz 6 – sterownik Firebird o nazwie JDBC wygenerował wyjątek typu [org.firebirdsql.jdbc.FBSQLException] z kodem błędu 335544336.
  • wiersz 7: wskazuje, że doszło do konfliktu dostępu między dwoma wątkami, które próbowały jednocześnie zaktualizować ten sam wiersz w tabeli [PERSONNES].

Nie jest to błąd nieodwracalny. Wątek, który przechwycił ten wyjątek, może ponowić próbę aktualizacji. W tym celu należy zmodyfikować kod funkcji [ThreadDaoMajEnfants]:

            try {
                 // zwiększa o 1 liczbę elementów potomnych tej kopii
                personne.setNbEnfants(nbEnfants + 1);
                 // próbuje się zmodyfikować oryginał
                dao.saveOne(personne);
                 // operacja zakończona  oryginał został zmodyfikowany
                fini = true;
            } catch (DaoException ex) {
                 // pobierany jest kod błędu
                codeErreur = ex.getCode();
                 // jeśli wystąpił błąd ID lub błąd o kodzie 2, ponawiamy próbę aktualizacji
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // nieobsługiwany wyjątek  pozwalamy na jego zgłoszenie
                    throw ex;
                }
  • wiersz 8: obsługujemy wyjątek typu [DaoException]. Zgodnie z powyższym powinniśmy obsłużyć wyjątek, który pojawił się podczas testów, czyli typ [org.springframework.jdbc.UncategorizedSQLException]. Nie możemy jednak poprzestać na obsłudze tego typu, który jest typem generycznym Springa przeznaczonym do hermetyzacji wyjątków, których nie rozpoznaje. Spring rozpoznaje wyjątki generowane przez sterowniki JDBC dla pewnej liczby systemów, takich jak Oracle, MySQL, Postgres, DB2, SQL Server, ... ale nie Firebird. W związku z tym każdy wyjątek zgłoszony przez sterownik JDBC serwera Firebird jest enkapsulowany w typie Spring [org.springframework.jdbc.UncategorizedSQLException]:

Image

Jak widać powyżej, klasa [UncategorizedSQLException] wywodzi się z klasy [DataAccessException], o której wspomnieliśmy w paragrafie 17.3.3. Można ustalić, jaki wyjątek został enkapsulowany w klasie [UncategorizedSQLException], korzystając z jej metody [getSQLException]:

Image

Ten wyjątek typu [SQLException] został wygenerowany przez warstwę [iBATIS], która z kolei zawiera wyjątek wygenerowany przez sterownik bazy danych JDBC. Dokładną przyczynę wyjątku typu [SQLException] można uzyskać za pomocą metody:

Image

Otrzymujemy obiekt typu [Throwable], który został wygenerowany przez sterownik JDBC:

Image

Typ [Throwable] jest klasą nadrzędną dla [Exception].

W tym przypadku musimy sprawdzić, czy obiekt typu [Throwable], wywołany przez sterownik JDBC z Firebirda i będący przyczynąwyjątku [SQLException] wygenerowanego przez warstwę [iBATIS], jest rzeczywiście wyjątkiem typu [org.firebirdsql.gds.GDSException] o kodzie błędu 335544336. Aby uzyskać kod błędu, możemy skorzystać z metody [getErrorCode()] klasy [org.firebirdsql.gds.GDSException].

Jeśli w kodzie [ThreadDaoMajEnfants] użyjemy wyjątku [org.firebirdsql.gds.GDSException], wówczas ten wątek będzie mógł współpracować wyłącznie z Firebirdem o numerze SGBD. To samo dotyczy testu [test4], który wykorzystuje ten wątek. Chcemy tego uniknąć. Chcemy bowiem, aby nasze testy JUnit pozostawały ważne niezależnie od używanego SGBD. Aby to osiągnąć, postanowiliśmy, że warstwa [dao] uruchomi [DaoException] o kodzie 4 w przypadku wykrycia wyjątku typu „konflikt aktualizacji”, niezależnie od tego, jaki jest bazowy SGBD. W ten sposób wątek [ThreadDaoMajEnfants] można przepisać w następujący sposób:

package istia.st.mvc.personnes.tests;
...

public class ThreadDaoMajEnfants extends Thread {
...

     // rdzeń wątku
    public void run() {
...
        while (!fini) {
             // pobieramy kopię osoby z idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
             // okres oczekiwania zakończony – próba zatwierdzenia kopii
             // w międzyczasie inne wątki mogły zmodyfikować oryginał
            int codeErreur = 0;
            try {
                 // zwiększono o 1 liczbę elementów potomnych tej kopii
                personne.setNbEnfants(nbEnfants + 1);
                 // próbujemy zmodyfikować oryginał
                dao.saveOne(personne);
                 // zostało to już zrobione – oryginał został zmodyfikowany
                fini = true;
            } catch (DaoException ex) {
                 // odzyskujemy kod błędu
                codeErreur = ex.getCode();
                 // jeśli wystąpi błąd ID lub wersji 2 albo zakleszczenie 4, to
                 // ponownie próbujemy zaktualizować
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                     // nieobsługiwany wyjątek – pozwala się na jego zgłoszenie
                    throw ex;
                }
            }
        }
         // monitorowanie
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • wiersze 34–36: wyjątek typu [DaoException] o kodzie 4 zostaje przechwycony. Wątek [ThreadDaoMajEnfants] zostanie zmuszony do ponownego rozpoczęcia procedury aktualizacji od początku (wiersz 10)

Nasza warstwa [dao] musi zatem być w stanie rozpoznać wyjątek typu „konflikt aktualizacji”. Wyjątek ten jest generowany przez sterownik JDBC i jest dla niego specyficzny. Wyjątek ten należy obsłużyć w metodzie [updatePersonne] klasy [DaoImplCommon]:

// zmiana danych osoby
    protected void updatePersonne(Personne personne) {
         // oczekuje się 10 ms  w celach testowych należy ustawić wartość true zamiast false
        if (true)
            wait(10);
         // zmiana
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

Wiersze 7–11 należy ująć w blok try / catch. W przypadku sterownika SGBD dla Firebirda należy sprawdzić, czy wyjątek, który spowodował niepowodzenie aktualizacji, jest typu [org.firebirdsql.gds.GDSException] i ma kod błędu 335544336. Jeśli umieścimy ten rodzaj testu w klasie [DaoImplCommon], powiążemy tę klasę z Firebirdem o numerze SGBD, co oczywiście nie jest pożądane. Jeśli chcemy zachować ogólny charakter klasy [DaoImplCommon], musimy ją wyprowadzić i obsłużyć wyjątek w klasie specyficznej dla Firebirda. Właśnie to teraz robimy.

17.4.2. Klasa [DaoImplFirebird]

Jej kod wygląda następująco:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

public class DaoImplFirebird extends DaoImplCommon {

     // edytuj osobę
    protected void updatePersonne(Personne personne) {
         // oczekiwanie 10 ms – w celu przeprowadzenia testów należy ustawić wartość „true” zamiast „false”
        if (true)
            wait(10);
         // zmiana
        try {
             // zmieniamy osobę, która ma prawidłową wersję
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

     // oczekiwanie
    private void wait(int N) {
         // czekamy N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // wyświetla się ślad wyjątku
            e.printStackTrace();
            return;
        }
    }

}
  • wiersz 5: klasa [DaoImplFirebird] wywodzi się z klasy [DaoImplCommon], którą właśnie omówiliśmy. W wierszach 8–33 redefiniuje ona metodę [updatePersonne], która sprawia nam problem.
  • wiersz 20: przechwytujemy wyjątek Spring typu [UncategorizedSQLException]
  • wiersze 21–22: sprawdzamy, czy przyczyną wyjątku bazowego typu [SQLException], wywołanego przez warstwę [iBATIS], jest wyjątek typu [org.firebirdsql.jdbc.FBSQLException]
  • wiersz 25: sprawdzamy ponadto, czy kod błędu tego wyjątku Firebird wynosi 335544336, czyli kod błędu „deadlock”.
  • wiersze 26–27: jeśli wszystkie te warunki są spełnione, uruchamiana jest operacja [DaoException] o kodzie 4.
  • wiersze 36–44: metoda [wait] pozwala zatrzymać bieżący wątek na N milisekund. Ma ona zastosowanie wyłącznie do celów testowych.

Jesteśmy gotowi do testowania nowej warstwy [dao].

17.4.3. Testy implementacji [DaoImplFirebird]

Plik konfiguracyjny testów [spring-config-test-dao-firebird.xml] został zmodyfikowany w celu wykorzystania implementacji [DaoImplFirebird]:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- źródło danych DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- uwaga: nie pozostawiaj spacji między dwoma tagami <value> -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasa dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • wiersz 32: nowa implementacja [DaoImplFirebird] warstwy [dao].

Wyniki testu [test4], który wcześniej zakończył się niepowodzeniem, są następujące:

Image

Test [test4] zakończył się powodzeniem. Ostatnie wiersze logów ekranowych są następujące:

1
2
3
4
5
6
7
thread n° 36 [1145977145984] : fin attente
thread n° 75 [1145977145984] : a terminé et passé le nombre d'enfants à 99
thread n° 36 [1145977146000] : version corrompue ou personne inexistante
thread n° 36 [1145977146000] : 99 -> 100 pour la version 100
thread n° 36 [1145977146000] : début attente
thread n° 36 [1145977146015] : fin attente
thread n° 36 [1145977146031] : a terminé et passé le nombre d'enfants à 100

Ostatni wiersz wskazuje, że jako ostatni zakończył się wątek nr 36. Wiersz 3 pokazuje konflikt wersji, który zmusił wątek nr 36 do wznowienia procedury aktualizacji danych osoby (wiersz 4). Inne wpisy w dzienniku wskazują na konflikty dostępu podczas aktualizacji:

1
2
3
thread n° 52 [1145977145765] : version corrompue ou personne inexistante
thread n° 75 [1145977145765] : conflit de mise à jour
thread n° 36 [1145977145765] : version corrompue ou personne inexistante

Wiersz 2 pokazuje, że wątek nr 75 nie zdołał przeprowadzić aktualizacji z powodu konfliktu aktualizacji: gdy polecenie SQL [update] zostało wysłane do tabeli [PERSONNES], wiersz, który należało zaktualizować, był zablokowany przez inny wątek. Ten konflikt dostępu zmusi wątek nr 75 do ponownej próby aktualizacji.

Na zakończenie, w przypadku [test4] można zauważyć znaczną różnicę w porównaniu z wynikami tego samego testu w wersji 1, gdzie test zakończył się niepowodzeniem z powodu problemów z synchronizacją. Ponieważ metody warstwy [dao] w wersji 1 nie były zsynchronizowane, pojawiały się konflikty dostępu. W tym przypadku nie musieliśmy synchronizować warstwy [dao]. Po prostu obsłużyliśmy konflikty dostępu zgłoszone przez Firebird.

Teraz uruchommy cały test JUnit warstwy [dao]:

Image

Wydaje się zatem, że mamy poprawną warstwę [dao]. Aby z dużym prawdopodobieństwem uznać ją za poprawną, musielibyśmy przeprowadzić więcej testów. Niemniej jednak uznamy ją za działającą.

17.5. Warstwa [service]

17.5.1. Składniki warstwy [service]

Warstwa [service] składa się z następujących klas i interfejsów:

Image

  • [IService] jest interfejsem udostępnianym przez warstwę [service]
  • [ServiceImpl] jest jej implementacją

Interfejs [IService] ma następującą postać:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // lista wszystkich osób
    Collection getAll();

     // wyświetlenie konkretnej osoby
    Personne getOne(int id);

     // dodaj/edytuj osobę
    void saveOne(Personne personne);

     // usunięcie osoby
    void deleteOne(int id);

     // zapisać wiele osób
    void saveMany(Personne[] personnes);

     // usunięcie wielu osób
    void deleteMany(int ids[]);
}
  • Interfejs posiada te same cztery metody, co w wersji 1, ale ma dodatkowo dwie:
    • saveMany: umożliwia atomowe zapisanie wielu osób jednocześnie. Albo wszystkie zostaną zapisane, albo żadna.
    • deleteMany: umożliwia usunięcie wielu osób jednocześnie w sposób atomowy. Albo wszystkie zostaną usunięte, albo żadna.

Te dwie metody nie będą wykorzystywane przez aplikację internetową. Dodaliśmy je, aby zilustrować pojęcie transakcji w bazie danych. Obie metody muszą bowiem zostać wykonane w ramach jednej transakcji, aby uzyskać pożądaną atomowość.

Klasa [ServiceImpl] implementująca ten interfejs będzie wyglądać następująco:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;

public class ServiceImpl implements IService {

     // warstwa [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // lista osób
    public Collection getAll() {
        return dao.getAll();
    }

     // wyświetlenie konkretnej osoby
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

     // dodaj lub edytuj osobę
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // usunięcie osoby
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

     // zapisanie zbioru osób
    public void saveMany(Personne[] personnes) {
         // przechodzenie po tablicy osób
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

     // usunięcie zbioru osób
    public void deleteMany(int[] ids) {
         // ids: identyfikatory osób do usunięcia
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • Metody [getAll, getOne, insertOne, saveOne] wywołują metody o tej samej nazwie z warstwy [dao].
  • wiersze 42–47: metoda [saveMany] zapisuje po jednej osobie z tablicy przekazanej jako parametr.
  • wiersze 50–55: metoda [deleteMany] usuwa pojedynczo osoby z tablicy przekazanej jako parametr przez metodę id

Wspomnieliśmy, że metody [saveMany] i [deleteMany] muszą być wykonywane w ramach transakcji, aby zapewnić zasadę „wszystko albo nic” dla tych metod. Możemy zauważyć, że powyższy kod całkowicie pomija pojęcie transakcji. Pojawi się ono dopiero w pliku konfiguracyjnym warstwy [service].

17.5.2. Konfiguracja warstwy [service]

Powyżej, w wierszu 11, widać, że implementacja [ServiceImpl] posiada odwołanie do warstwy [dao]. Warstwa ta, podobnie jak w wersji 1, zostanie zainicjowana przez Spring podczas instancjonowania warstwy [service - ServiceImpl]. Plik konfiguracyjny umożliwiający instancjonowanie warstwy [service] będzie wyglądał następująco:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- źródło danych DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- uwaga: nie pozostawiaj spacji między dwoma tagami <value> -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasa dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- menedżer transakcji -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- klasy dostępu do warstwy [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • wiersze 1–36: konfiguracja warstwy [dao]. Konfiguracja ta została wyjaśniona podczas omówienia warstwy [dao] w paragrafie 17.3.2.
  • wiersze 38–64: konfigurują warstwę [service]

W wierszu 46 widać, że implementacja warstwy [service] jest realizowana przez typ [TransactionProxyFactoryBean]. Spodziewaliśmy się znaleźć typ [ServiceImpl]. [TransactionProxyFactoryBean] jest typem predefiniowanym przez Spring. Jak to możliwe, że typ predefiniowany może implementować interfejs [IService], który jest specyficzny dla naszej aplikacji?

Przyjrzyjmy się najpierw klasie [TransactionProxyFactoryBean]:

Image

Widzimy, że implementuje ona interfejs [FactoryBean]. Z tym interfejsem mieliśmy już do czynienia. Wiemy, że gdy aplikacja zwraca się do Springa z prośbą o instancję typu implementującego [FactoryBean], Spring zwraca nie instancję typu [I], ale obiekt zwrócony przez metodę [I].getObject():

Image

W naszym przypadku warstwa [service] zostanie zaimplementowana przez obiekt zwracany przez metodę [TransactionProxyFactoryBean].getObject(). Jaki jest charakter tego obiektu? Nie będziemy wchodzić w szczegóły, ponieważ są one skomplikowane. Dotyczą one tak zwanego Spring AOP (programowania zorientowanego na aspekty). Postaramy się wyjaśnić tę kwestię za pomocą prostych schematów. AOP umożliwia następujące działanie:

  • mamy dwie klasy C1 i C2, przy czym C1 korzysta z interfejsu [I2] udostępnionego przez C2:
  • dzięki AOP można umieścić, w sposób przezroczysty dla obu klas, interceptor pomiędzy klasami C1 i C2:

Klasa [C1] została skompilowana do współpracy z interfejsem [I2], który implementuje klasa [C2]. W czasie wykonywania AOP umieszcza klasę [intercepteur] pomiędzy klasami [C1] i [C2]. Aby było to możliwe, klasa [intercepteur] musi oczywiście udostępniać klasie [C1] ten sam interfejs [I2], co klasa [C2].

Do czego to może służyć? Dokumentacja Springa podaje kilka przykładów. Można na przykład chcieć generować logi przy wywołaniach konkretnej metody M klasy [C2], aby przeprowadzić audyt tej metody. W [intercepteur] należy zatem napisać metodę [M], która generuje te logi. Wywołanie metody M z [C1] w [C2] będzie przebiegało w następujący sposób (patrz schemat powyżej):

  1. [C1] wywołuje metodę M z [C2]. W rzeczywistości wywołana zostanie metoda M klasy [intercepteur]. Jest to możliwe, jeśli klasa [C1] odwołuje się do interfejsu [I2], a nie do konkretnej implementacji klasy [I2]. Wystarczy zatem, aby [intercepteur] implementowało [I2].
  2. Metoda M klasy [intercepteur] generuje logi i wywołuje metodę M klasy [C2], do której pierwotnie odwoływała się klasa [C1].
  3. Metoda M klasy [C2] jest wykonywana i zwraca swój wynik do metody M klasy [intercepteur], która ewentualnie może dodać coś do tego, co zostało wykonane w punkcie 2.
  4. Metoda M klasy [intercepteur] zwraca wynik do wywołującej metody klasy [C1]

Widać, że metoda M klasy [intercepteur] może wykonać pewne czynności przed i po wywołaniu metody M klasy [C2]. W odniesieniu do klasy [C1] rozszerza ona zatem metodę M klasy [C2]. Technologię AOP można zatem postrzegać jako sposób na rozszerzenie interfejsu prezentowanego przez klasę.

Jak ta koncepcja ma zastosowanie do naszej warstwy [service]? Jeśli zaimplementujemy warstwę [service] bezpośrednio za pomocą instancji [ServiceImpl], nasza aplikacja internetowa będzie miała następującą architekturę:

Jeśli zaimplementujemy warstwę [service] za pomocą instancji [TransactionProxyFactoryBean], otrzymamy następującą architekturę:

Można powiedzieć, że warstwa [service] jest instancjonowana za pomocą dwóch obiektów:

  • obiekt, który powyżej nazwaliśmy [proxy transactionnel] i który w rzeczywistości jest obiektem zwracanym przez metodę [getObject] obiektu [TransactionProxyFactoryBean]. To właśnie ten obiekt będzie pełnił rolę interfejsu między warstwą [service] a warstwą [web]. Z założenia implementuje on interfejs [IService].
  • instancja [ServiceImpl], która również implementuje interfejs [IService]. Tylko ona wie, jak współpracować z warstwą [dao], dlatego jest niezbędna.

Załóżmy, że warstwa [web] wywołuje metodę [saveMany] z interfejsu [IService]. Wiemy, że z funkcjonalnego punktu widzenia operacje dodawania/aktualizacji wykonywane przez tę metodę muszą odbywać się w ramach transakcji. Albo wszystkie zakończą się powodzeniem, albo żadna z nich nie zostanie wykonana. Przedstawiliśmy metodę [saveMany] klasy [ServiceImpl] i zwróciliśmy uwagę na fakt, że nie obsługuje ona transakcji. Metoda [saveMany] z klasy [proxy transactionnel] wzbogaci metodę [saveMany] z klasy [ServiceImpl] o tę koncepcję transakcji. Przyjrzyjmy się powyższemu schematowi:

  1. warstwa [web] wywołuje metodę [saveMany] z interfejsu [IService].
  2. Wykonana zostaje metoda [saveMany] z klasy [proxy transactionnel]. Rozpoczyna ona transakcję. Musi ona dysponować wystarczającymi informacjami, aby to zrobić, w szczególności obiektem [DataSource], aby uzyskać połączenie z SGBD. Następnie wywołuje metodę [saveMany] z [ServiceImpl].
  3. Ta ostatnia zostaje wykonana. Wielokrotnie wywołuje ona warstwę [dao] w celu wykonania operacji wstawiania lub aktualizacji. Polecenia SQL wykonywane przy tej okazji są realizowane w transakcji rozpoczętej w punkcie 2.
  4. Załóżmy, że jedna z tych operacji zakończy się niepowodzeniem. Warstwa [dao] pozwoli na przekazanie wyjątku do warstwy [service], a konkretnie do metody [saveMany] instancji [ServiceImpl].
  5. Ta nie podejmuje żadnych działań i pozwala, by wyjątek został przekazany dalej do metody [saveMany] w instancji [proxy transactionnel].
  6. Po otrzymaniu wyjątku metoda [saveMany] z instancji [proxy transactionnel], która jest właścicielem transakcji, wykonuje metodę [rollback] tej transakcji w celu anulowania wszystkich aktualizacji, a następnie pozwala, by wyjątek został przekazany w górę do warstwy [web], która będzie odpowiedzialna za jego obsługę.

W kroku 4 założyliśmy, że jedno z wstawień lub aktualizacji zakończyło się niepowodzeniem. Jeśli tak nie jest, w [5] nie jest zgłaszany żaden wyjątek. To samo dotyczy [6]. W takim przypadku metoda [saveMany] z klasy [proxy transactionnel] wykonuje operację [commit] na transakcji w celu zatwierdzenia wszystkich aktualizacji.

Mamy teraz dokładniejsze wyobrażenie o architekturze wdrożonej przez bean [TransactionProxyFactoryBean]. Wróćmy do jego konfiguracji:


    <!-- menedżer transakcji -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- klasy dostępu do warstwy [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

Przeanalizujmy tę konfigurację w świetle skonfigurowanej architektury:

  • [proxy transactionnel] będzie zarządzać transakcjami. Spring oferuje kilka strategii zarządzania nimi. [proxy transactionnel] wymaga odwołania do wybranego menedżera transakcji.
  • wiersze 11–13: definiują atrybut [transactionManager] fasoli [TransactionProxyFactoryBean] wraz z odwołaniem do menedżera transakcji. Jest on zdefiniowany w wierszach 2–7.
  • wiersze 2–7: menedżer transakcji jest typu [DataSourceTransactionManager]:

Image

[DataSourceTransactionManager] jest menedżerem transakcji dostosowanym do obiektów SGBD, do których dostęp uzyskuje się za pośrednictwem obiektu [DataSource]. Potrafi on obsługiwać wyłącznie transakcje na jednym obiekcie SGBD. Nie obsługuje transakcji rozproszonych na wielu obiektach SGBD. W tym przypadku mamy do czynienia tylko z jednym obiektem SGBD. Dlatego ten menedżer transakcji jest odpowiedni. Kiedy [proxy transactionnel] rozpocznie transakcję, zrobi to na połączeniu przypisanym do wątku. To właśnie to połączenie będzie wykorzystywane we wszystkich warstwach prowadzących do bazy danych: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

Klasa [DataSourceTransactionManager] musi znać źródło danych, do którego ma zażądać połączenia w celu przypisania go do wątku. Jest ono zdefiniowane w wierszach 4–6: jest to to samo źródło danych, co to używane przez warstwę [dao] (patrz paragraf 17.5.2).

  • wiersze 14–19: atrybut „target” wskazuje klasę, która ma zostać przechwycona, w tym przypadku klasę [ServiceImpl]. Informacja ta jest niezbędna z dwóch powodów:
    • klasa [ServiceImpl] musi zostać zainicjowana, ponieważ to ona zapewnia komunikację z warstwą [dao]
    • [TransactionProxyFactoryBean] musi wygenerować proxy, które udostępnia warstwie [web] ten sam interfejs co [ServiceImpl].
  • wiersze 21–27: wskazują, które metody klasy [ServiceImpl] proxy musi przechwytywać. Atrybut [transactionAttributes] w wierszu 21 wskazuje, które metody klasy [ServiceImpl] wymagają transakcji oraz jakie są jej atrybuty:
  • wiersz 23: metody, których nazwy zaczynają się od „get” [getOne, getAll], są wykonywane w transakcji o atrybucie [PROPAGATION_REQUIRED,readOnly]:
    • PROPAGATION_REQUIRED: metoda jest wykonywana w transakcji, jeśli do wątku jest już przypisana transakcja; w przeciwnym razie tworzona jest nowa transakcja i metoda jest wykonywana w jej ramach.
    • readOnly: transakcja tylko do odczytu

W tym przypadku metody [getOne] i [getAll] z [ServiceImpl] będą wykonywane w transakcji, podczas gdy w rzeczywistości nie jest to konieczne. W każdym z tych przypadków chodzi o operację składającą się z pojedynczego polecenia SELECT. Nie widać sensu umieszczania tego SELECT w transakcji.

  • wiersz 24: metody, których nazwy zaczynają się od „save”, takie jak [saveOne, saveMany], są wykonywane w transakcji o atrybucie [PROPAGATION_REQUIRED].
  • wiersz 25: metody [deleteOne] i [deleteMany] z klasy [ServiceImpl] są skonfigurowane identycznie jak metody [saveOne, saveMany].

W naszej warstwie [service] tylko metody [saveMany] i [deleteMany] muszą być wykonywane w ramach transakcji. Konfigurację można by ograniczyć do następujących wierszy:


        <property name="transactionAttributes">
            <props>
                <prop key="saveMany">PROPAGATION_REQUIRED</prop>
                <prop key="deleteMany">PROPAGATION_REQUIRED</prop>
            </props>
</property>

17.6. Testy warstwy [service]

Teraz, gdy napisaliśmy i skonfigurowaliśmy warstwę [service], zamierzamy ją przetestować za pomocą testów JUnit:

Image

Plik konfiguracyjny [spring-config-test-service-firebird.xml] warstwy [service] jest tym samym, który został opisany w paragrafie 17.5.2.

Test JUnit [TestServiceFirebird] wygląda następująco:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

     // warstwa [service]
    private IService service;

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

     // konfiguracja
    public void setUp() {
        service = (IService) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-service-firebird.xml"))).getBean("service");
    }

     // lista osób
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modyfikacja lub usunięcie nieistniejącego elementu
    public void test2() throws ParseException {
...
    }

     // zarządzanie wersjami osób
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blokowanie optymistyczne – dostęp wielowątkowy
    public void test4() throws Exception {
...
    }

     // testy poprawności saveOne
    public void test5() throws ParseException {
...
    }

         // wstawianie wielowątkowe
    public void test6() throws ParseException, InterruptedException{
...
    }

     // testy metody deleteMany
    public void test7() throws ParseException {
         // aktualna lista
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
         // wyświetlanie
        doListe(personnes);
         // utworzenie trzech osób
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
         // dodanie 3 osób – osoba p3 o identyfikatorze -2 sp owoduje
         // wyjątek
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // weryfikacja
        assertTrue(erreur);
         // nowa lista – liczba elementów nie powinna ulec zmianie
         // z powodu automatycznego cofnięcia transakcji
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
         // dodano dwie prawidłowe osoby
         // przywracamy ich identyfikatory do wartości -1
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
         // odzyskujemy ich identyfikatory
        int id1 = p1.getId();
        int id2 = p2.getId();
         // weryfikacje
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
         // nowa lista – musi zawierać co najmniej 2 elementy
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
         // usuwamy p1 i p2 oraz nieistniejącą osobę
         // powinien wystąpić wyjątek
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // weryfikacja
        assertTrue(erreur);
         // nowa lista
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
         // żadna osoba nie powinna zostać usunięta (automatyczne cofnięcie
         // automatyczne cofnięcie transakcji)
        assertEquals(nbPersonnes4, nbPersonnes3);
         // usuwamy dwie prawidłowe osoby
        service.deleteMany(new int[] { id1, id2 });
         // weryfikacje
         // osoba p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // powinien wystąpić błąd o kodzie 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // osoba p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // powinien wystąpić błąd o kodzie 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // nowa lista
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
         // weryfikacja – powinniśmy wrócić do punktu wyjścia
        assertEquals(nbPersonnes5, nbPersonnes1);
         // wyświetlanie
        doListe(personnes);
    }

}
  • wiersze 19–22: program testuje warstwy [dao] i [service] skonfigurowane przez plik [spring-config-test-service-firebird.xml], omówiony w poprzedniej sekcji.
  • Testy od [test1] do [test6] są zasadniczo identyczne jak ich odpowiedniki o tej samej nazwie w klasie testowej [TestDaoFirebird] warstwy [dao]. Jedyną różnicą jest to, że zgodnie z konfiguracją metody [saveOne] i [deleteOne] są teraz wykonywane w ramach transakcji.
  • Metoda [test7] ma na celu przetestowanie metod [saveMany] i [deleteMany]. Chcemy sprawdzić, czy rzeczywiście są one wykonywane w ramach transakcji. Skomentujmy kod tej metody:
  • wiersze 62–63: zliczamy liczbę osób [nbPersonnes1] znajdujących się obecnie na liście
  • wiersze 67–72: tworzone są trzy osoby
  • wiersze 73–83: te trzy osoby są zapisywane za pomocą metody [saveMany] – wiersz 77. Dwie pierwsze osoby, p1 i p2, o identyfikatorze równym -1, zostaną dodane do tabeli [PERSONNES]. Osoba p3 ma natomiast identyfikator równy -2. Nie jest to zatem operacja wstawienia, lecz aktualizacji. Operacja ta zakończy się niepowodzeniem, ponieważ w tabeli [PERSONNES] nie ma żadnej osoby o identyfikatorze równym -2. Warstwa [dao] wygeneruje zatem wyjątek, który zostanie przekazany do warstwy [service]. Istnienie tego wyjątku jest sprawdzane w wierszu 83.
  • Z powodu poprzedniego wyjątku warstwa [service] powinna utworzyć warstwę [rollback] zawierającą wszystkie polecenia SQL wysłane podczas wykonywania metody [saveMany], ponieważ metoda ta jest wykonywana w ramach transakcji. W wierszach 86–87 sprawdzamy, czy liczba osób na liście nie uległa zmianie, a zatem czy nie doszło do wstawienia p1 i p2.
  • wiersze 88–103: dodaje się wyłącznie osoby p1 i p2, a następnie sprawdza się, czy na liście znajdują się dwie dodatkowe osoby.
  • wiersze 106–114: usuwa się grupę osób składającą się z osób p1 i p2, które właśnie zostały dodane, oraz z osoby nieistniejącej (id = -1). W tym celu wykorzystywana jest metoda [deleteMany], wiersz 108. Metoda ta zakończy się niepowodzeniem, ponieważ w tabeli [PERSONNES] nie ma żadnej osoby o identyfikatorze równym –1. Warstwa [dao] wygeneruje zatem wyjątek, który zostanie przekazany do warstwy [service]. Istnienie tego wyjątku jest sprawdzane w wierszu 114.
  • Z powodu poprzedniego wyjątku warstwa [service] powinna utworzyć warstwę [rollback] zawierającą wszystkie polecenia SQL wygenerowane podczas wykonywania metody [deleteMany], ponieważ metoda ta jest wykonywana w ramach transakcji. W wierszach 116–117 sprawdzamy, czy liczba osób na liście nie uległa zmianie, a zatem usunięcie p1 i p2 nie miało miejsca.
  • Wiersz 122: usuwa się grupę składającą się wyłącznie z osób p1 i p2. Operacja ta powinna zakończyć się powodzeniem. Pozostała część metody sprawdza, czy rzeczywiście tak się stało.

Wykonanie testów daje następujące wyniki:

Image

Wszystkie siedem testów zakończyło się powodzeniem. Uznamy naszą warstwę [service] za gotową do działania.

17.7. Warstwa [web]

Przypomnijmy ogólną architekturę tworzonej aplikacji internetowej:

Właśnie stworzyliśmy warstwy [dao] i [service], umożliwiające pracę z bazą danych Firebird. Napisaliśmy wersję 1 tej aplikacji, w której warstwy [dao] i [service] korzystały z listy osób przechowywanej w pamięci. Warstwa [web] napisana przy tej okazji pozostaje aktualna. Była ona bowiem przeznaczona dla warstwy [service], która implementowała interfejs [IService]. Ponieważ nowa warstwa [service] implementuje ten sam interfejs, warstwa [web] nie wymaga modyfikacji.

W poprzednim artykule wersja 1 aplikacji została przetestowana z projektem Eclipse o nazwie [mvc-personnes-02B], w którym warstwy [web, service, dao, entites] zostały umieszczone w plikach .jar:

Folder [src] był pusty. Klasy warstw znajdowały się w archiwach [personnes-*.jar ]:

Aby przetestować wersję 2, w środowisku Eclipse duplikujemy folder Eclipse [mvc-personnes-02B] do [mvc-personnes-03B] (kopiuj/wklej):

Image

W projekcie [mvc-personnes-03] eksportujemy warstwy [dao] i [service] odpowiednio do archiwów [personnes-dao.jar] i [personnes-service.jar] w folderze [dist] w ramach projektu:

Image

Kopiujemy te dwa pliki, a następnie w programie Eclipse wklejamy je do folderu [WEB-INF/lib] projektu [mvc-personnes-03B], gdzie zastąpią one archiwa o tej samej nazwie z poprzedniej wersji.

Kopiujemy i wklejamy również archiwa [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] z folderu [lib] projektu [mvc-personnes-03] do folderu [WEB-INF/lib] projektu [mvc-personnes-03B]. Archiwa te są niezbędne dla nowych warstw [dao] i [service].

Po wykonaniu tej czynności dodajemy nowe archiwa do ścieżki Classpath projektu: [clic droit sur projet -> Properties -> Java Build Path -> Add Jars].

Folder [src] zawiera pliki konfiguracyjne warstw [dao] i [service]:

Image

Plik [spring-config.xml] konfiguruje warstwy [dao] i [service] aplikacji internetowej. W nowej wersji jest on identyczny z plikiem [spring-config-test-service-firebird.xml], który posłużył do skonfigurowania testu warstwy usługowej w projekcie [mvc-personnes-03]. Wystarczy więc skopiować i wkleić zawartość jednego pliku do drugiego:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- źródło danych DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- uwaga: nie należy pozostawiać spacji między dwoma tagami <value> -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- klasa dostępu do warstwy [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- menedżer transakcji -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- klasy dostępu do warstwy [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • wiersz 12: adres URL bazy danych Firebird. Nadal korzystamy z bazy, która służyła do testowania warstw [dao] i [service]

Wdrażamy projekt internetowy [mvc-personnes-03B] w środowisku Tomcat:

Jesteśmy gotowi do przeprowadzenia testów typu „ ”. Uruchamiany jest serwer Firebird o nazwie SGBD. Zawartość tabeli [PERSONNES] wygląda wówczas następująco:

Image

Następnie uruchamiany jest Tomcat. W przeglądarce wpisujemy adres URL [http://localhost:8080/mvc-personnes-03B]:

Image

Dodajemy nową osobę za pomocą linku [Ajout]:

Sprawdzamy, czy wpis został dodany do bazy danych:

Image

Użytkownik jest proszony o przeprowadzenie kolejnych testów [modification, suppression].

Przeprowadźmy teraz test konfliktów wersji, który został przeprowadzony w wersji 1. [Firefox] będzie przeglądarką użytkownika U1. Użytkownik ten żąda adresu URL [http://localhost:8080/mvc-personnes-03B]:

Image

[IE] będzie przeglądarką użytkownika U2. Użytkownik ten wysyła żądanie do tego samego adresu URL:

Image

Użytkownik U1 przechodzi do edycji danych osoby [Perrichon]:

Image

Użytkownik U2 postępuje analogicznie:

Image

Użytkownik U1 wprowadza zmiany i zatwierdza je:

Użytkownik U2 postępuje tak samo:

Użytkownik U2 wraca do listy osób za pomocą linku [Annuler] z formularza:

Image

Znajduje osobę o identyfikatorze [Perrichon] w wersji zmodyfikowanej przez użytkownika U1 (nazwisko zamieniono na wielkie litery).

A co z bazą danych w tym wszystkim? Sprawdźmy:

Image

Nazwisko osoby nr 899 jest rzeczywiście zapisane wielkimi literami w wyniku zmiany dokonanej przez U1.

17.8. Conclusion

Przypomnijmy sobie, co chcieliśmy osiągnąć. Mieliśmy aplikację internetową o następującej architekturze trójwarstwowej:

gdzie warstwy [dao] i [service] pracowały z listą danych przechowywaną w pamięci, która była zatem tracona po wyłączeniu serwera internetowego. Była to wersja 1. W wersji 2 warstwy [service] i [dao] zostały przepisane tak, aby lista osób znajdowała się w tabeli bazy danych. Dzięki temu jest ona teraz trwała. Teraz zamierzamy sprawdzić, jaki wpływ na naszą aplikację ma zmiana w module SGBD. W tym celu stworzymy trzy nowe wersje naszej aplikacji internetowej:

  • wersja 3: SGBD korzysta z Postgres
  • wersja 4: SGBD to MySQL
  • wersja 5: SGBD to SQL Server Express 2005

Zmiany wprowadzono w następujących miejscach:

  • klasa [DaoImplFirebird] implementuje funkcje warstwy [dao] związane z SGBD Firebird. Jeśli potrzeba ta będzie nadal występować, zostanie ona zastąpiona odpowiednio przez klasy [DaoImplPostgres], [DaoImplMySQL] i [DaoImplSqlExpress].
  • Plik mapowania [personnes-firebird.xml] z iBATIS dla Firebird o numerze SGBD zostanie zastąpiony odpowiednio przez pliki mapowania [personnes-postgres.xml], [personnes-mysql.xml] oraz [personnes-sqlexpress.xml].
  • Konfiguracja obiektu [DataSource] w warstwie [dao] jest specyficzna dla danego SGBD. W związku z tym będzie się zmieniać w każdej wersji.
  • Sterownik JDBC obiektu SGBD również zmienia się w każdej wersji

Poza tymi kwestiami wszystko pozostaje bez zmian. W dalszej części opisujemy te nowe wersje, skupiając się wyłącznie na nowościach wprowadzonych przez każdą z nich.