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]:

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:
- 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:

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
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]:

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]:

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:
![]()
- [dbpersonnes.gdb] to baza danych.
- [dbpersonnes.sql] to skrypt SQL służący do generowania bazy:
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:
![]()
- [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:

- [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:
- Interfejs posiada te same cztery metody, co w poprzedniej wersji.
Klasa [DaoImplCommon] implementująca ten interfejs będzie wyglądać następująco:
- 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/]:

[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]:

![]() |
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:

Wśród metod tej klasy jedna z nich pozwala skonfigurować klienta [iBATIS], za pomocą którego będziemy korzystać z bazy danych:
![]()
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:
- zgłoszenie żądania połączenia do puli połączeń
- otwarcie transakcji
- wykonanie serii poleceń SQL zapisanych w pliku konfiguracyjnym
- zamknięcie transakcji
- 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:

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

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()]:
![]()
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:

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]:

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/]:

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:

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:
- nazwę sterownika JDBC, który ma być używany – zainicjowanego za pomocą [setDriverClassName]
- nazwę adresu URL bazy danych, z której ma korzystać – zainicjowaną jako [setUrl]
- 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ć)
- 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:

- [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:
Przeanalizujemy te metody po kolei.
getAll
Ta metoda pozwala uzyskać wszystkie osoby z listy. Jej kod jest następujący:
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ę:
![]()
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:
![]()
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:

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

Interfejs ten definiuje metody umożliwiające wykorzystanie zawartości pliku [personnes-firebird.xml]:
[queryForList]
![]()
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]
![]()
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]
![]()
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]
![]()
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]
![]()
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]:
- 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:
- 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:
- 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:
- 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:
- 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.
- 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:
- 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:
- Aktualizacja może się nie powieść z co najmniej dwóch powodów:
- osoba, którą należy zaktualizować, nie istnieje
- 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:

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]:
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:

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

Udało się uzyskać listę osób. Można przejść do testu JUnit.
Test JUnit [TestDaoFirebird] wygląda następująco:
- 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:
- 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.
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:
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:
- 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:
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:
- 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]:
- 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]:

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]:
![]()
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:
![]()
Otrzymujemy obiekt typu [Throwable], który został wygenerowany przez sterownik JDBC:

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:
- 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]:
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:
- 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:

Test [test4] zakończył się powodzeniem. Ostatnie wiersze logów ekranowych są następujące:
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:
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]:

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:
![]()
- [IService] jest interfejsem udostępnianym przez warstwę [service]
- [ServiceImpl] jest jej implementacją
Interfejs [IService] ma następującą postać:
- 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:
- 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]:

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():
![]()
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):
- [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].
- Metoda M klasy [intercepteur] generuje logi i wywołuje metodę M klasy [C2], do której pierwotnie odwoływała się klasa [C1].
- 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.
- 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:
- warstwa [web] wywołuje metodę [saveMany] z interfejsu [IService].
- 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].
- 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.
- 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].
- Ta nie podejmuje żadnych działań i pozwala, by wyjątek został przekazany dalej do metody [saveMany] w instancji [proxy transactionnel].
- 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]:

[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:

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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
- 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:

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):

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:

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]:

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:

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

Dodajemy nową osobę za pomocą linku [Ajout]:
![]() | ![]() |
Sprawdzamy, czy wpis został dodany do bazy danych:

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]:

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

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

Użytkownik U2 postępuje analogicznie:

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:

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:

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.

























