19. Aplikacja internetowa MVC w architekturze trójwarstwowej – przykład 5, MySQL
19.1. Baza danych MySQL
W tej wersji zamieścimy listę osób w tabeli bazy danych MySQL 4.x. Wykorzystaliśmy pakiet [Apache – MySQL – PHP] dostępny pod adresem [http://www.easyphp.org]. Poniższe zrzuty ekranu pochodzą z klienta EMS, MySQL Manager Lite oraz [http://www.sqlmanager.net/fr/products/mysql/manager], bezpłatnego klienta administracyjnego SGBD MySQL.
Baza danych nosi nazwę [dbpersonnes]. Zawiera ona tabelę [PERSONNES]:

Tabela [PERSONNES] będzie zawierała listę osób zarządzanych przez aplikację internetową. Została ona utworzona za pomocą następujących poleceń SQL:
MySQL 4.x wydaje się mniej rozbudowany niż dwa poprzednie SGBD. Nie udało mi się zastosować ograniczeń (checks) w tej tabeli.
- wiersz 10: tabela musi mieć typ [InnoDB], a nie typ [MyISAM], który nie obsługuje transakcji.
- wiersz 2: klucz podstawowy ma typ auto_increment. Jeśli wstawimy wiersz bez wartości w kolumnie ID tabeli, MySQL automatycznie wygeneruje liczbę całkowitą dla tej kolumny. Dzięki temu nie będziemy musieli samodzielnie generować kluczy podstawowych.
Tabela [PERSONNES] mogłaby mieć następującą zawartość:

Wiemy, że podczas wstawiania obiektu [Personne] przez naszą warstwę [dao], pole [id] tego obiektu przed wstawieniem ma wartość -1, a po wstawieniu ma wartość inną niż -1, przy czym wartość ta jest kluczem głównym przypisanym do nowego wiersza wstawionego do tabeli [PERSONNES]. Na przykładzie zobaczmy, w jaki sposób możemy poznać tę wartość.
![]() |
![]() |
Zlecenie SQL
pozwala ustalić ostatnią wartość wprowadzoną do pola ID w tabeli. Należy ją wygenerować po wstawieniu danych. Różni się to od poleceń SGBD, [Firebird] i [Postgres], w których przed wstawieniem żądano wartości klucza głównego dodanej osoby. Wykorzystamy to w pliku [personnes-mysql.xml], który gromadzi polecenia SQL wysłane do bazy danych.
19.2. Projekt Eclipse warstw [dao] i [service]
Aby rozwinąć warstwy [dao] i [service] naszej aplikacji z bazą danych MySQL, wykorzystamy następujący projekt Eclipse [mvc-personnes-05]:

Projekt ten jest prostym projektem Java, a nie projektem internetowym opartym na Tomcat.
Folder [src]
Ten folder zawiera kod źródłowy warstw [dao] i [service], a także pliki konfiguracyjne tych dwóch warstw:

Wszystkie pliki, których nazwa zawiera [mysql], mogły zostać zmodyfikowane lub pozostać bez zmian w stosunku do wersji Firebird i Postgres. Poniżej opisujemy te, które zostały zmodyfikowane.
Folder [database]
Ten folder zawiera skrypt służący do utworzenia bazy danych osób o nazwie MySQL:
![]()
Folder [lib]
Ten folder zawiera archiwa niezbędne do działania aplikacji:
![]() |
Warto zwrócić uwagę na obecność sterownika JDBC o nazwie SGBD MySQL. Wszystkie te pliki archiwum stanowią część projektu Eclipse o nazwie Classpath.
19.3. Warstwa [dao]
Warstwa [dao] wygląda następująco:

Przedstawiamy jedynie zmiany w stosunku do wersji [Firebird].
Plik mapowania [personne-mysql.xml] ma następującą postać:
<?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="istia.st.mvc.personnes.entites.Personne">
<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">
insert into
PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS)
VALUES(#wersja#, #nazwisko#, #imię#, #dateNaissance#, #mąż#,
#nbEnfants#)
<selectKey keyProperty="id">
select LAST_INSERT_ID() as value
</selectKey>
</insert>
<!-- 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>
<!-- usunięcie osoby -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#wartość# </delete>
<!-- pobierz wartość klucza głównego [id] ostatniej dodanej osoby -->
<select id="Personne.getNextId" resultClass="int">select
LAST_INSERT_ID()</select>
</sqlMap>
Zawartość jest taka sama jak w pliku [personnes-firebird.xml], z wyjątkiem następujących szczegółów:
- kolejność SQL „Personne.insertOne” uległa zmianie w wierszach 29–37:
- polecenie wstawiania SQL jest wykonywane przed poleceniem SELECT, które umożliwi pobranie wartości klucza głównego z wstawionego wiersza
- polecenie wstawiania SQL nie zawiera wartości dla kolumny ID w tabeli [PERSONNES]
Odzwierciedla to przykład wstawiania, który omówiliśmy w paragrafie 19.1.
Należy zauważyć, że może to stanowić potencjalne źródło problemów między współbieżnymi wątkami. Wyobraźmy sobie dwa wątki Th1 i Th2, które jednocześnie dokonują wstawienia. Łącznie należy wygenerować cztery polecenia SQL. Załóżmy, że zostaną one wykonane w następującej kolejności:
- wstawienie I1 przez Th1
- wstawienie I2 przez Th2
- polecenie select S1 wykonane przez wątek Th1
- pobranie S2 z Th2
W punkcie 3 Th1 pobiera klucz podstawowy wygenerowany podczas ostatniego wstawienia, a więc klucz Th2, a nie swój własny. Nie wiem, czy metoda [insert] klasy iBATIS jest zabezpieczona na taki przypadek. Załóżmy, że radzi sobie z tym poprawnie. Gdyby tak nie było, musielibyśmy utworzyć klasę implementacyjną [DaoImplCommon] na podstawie warstwy [dao], tworząc klasę [DaoImplMySQL], w której metoda [insertPersonne] byłaby zsynchronizowana. Rozwiązałoby to problem jedynie w odniesieniu do wątków naszej aplikacji. Jeśli natomiast Th1 i Th2 są wątkami dwóch różnych aplikacji, należałoby rozwiązać ten problem zarówno za pomocą transakcji, jak i odpowiedniego poziomu izolacji (isolation level) między transakcjami. Odpowiedni byłby poziom [serializable], w którym transakcje są wykonywane tak, jakby przebiegały sekwencyjnie.
Należy zauważyć, że problem ten nie występuje w przypadku Firebirda i Postgresa, które wykonują SELECT przed INSERT. Jeśli na przykład mamy sekwencję:
- select S1 z Th1
- select S2 z Th2
- wstawienie I1 z Th1
- wstawienie I2 z Th2
W krokach 1 i 2 Th1 i Th2 pobierają wartości klucza głównego z tego samego generatora. Operacja ta jest zazwyczaj atomowa, a Th1 i Th2 otrzymają dwie różne wartości. Gdyby operacja nie była atomowa, a Th1 i Th2 pobrałyby dwie identyczne wartości, wstawienie wykonane w kroku 4 przez Th2 zakończyłoby się niepowodzeniem z powodu duplikatu klucza głównego. Jest to błąd, który można całkowicie naprawić, a Th2 może ponowić próbę wstawienia.
Pozostawimy operację „Personne.insertOne” w obecnej postaci w pliku [personnes-mysql.xml], ale czytelnik powinien mieć świadomość, że potencjalnie istnieje tu problem.
Klasa implementacyjna [DaoImplCommon] warstwy [dao] jest taka sama jak w dwóch poprzednich wersjach.
Konfiguracja warstwy [dao] została dostosowana do SGBD i [MySQL]. W związku z tym plik konfiguracyjny [spring-config-test-dao-mysql.xml] ma następującą postać:
<?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>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost/dbpersonnes</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value></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-mysql.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 5–19: bean [dataSource] odnosi się teraz do bazy [MySQL] [dbpersonnes], której administratorem jest [root] bez hasła. Użytkownik powinien dostosować tę konfigurację do własnego środowiska.
- wiersz 31: klasa [DaoImplCommon] jest klasą implementacyjną warstwy [dao]
Po wprowadzeniu tych zmian można przejść do testów.
19.4. Testy warstw [dao] i [service]
Testy warstw [dao] i [service] są takie same jak dla wersji [Firebird]. Uzyskane wyniki są następujące:
![]() |
Stwierdzono, że testy zakończyły się powodzeniem w przypadku implementacji [DaoImplCommon]. Nie będziemy musieli tworzyć klasy pochodnej, jak to było konieczne w przypadku SGBD i [Firebird].
19.5. Testy aplikacji [web]
Aby przetestować aplikację internetową z wykorzystaniem SGBD i [MySQL], tworzymy projekt Eclipse o nazwie [mvc-personnes-05B] w sposób analogiczny do tego, który zastosowano przy tworzeniu projektu [mvc-personnes-03B] z bazą danych Firebird (patrz paragraf 17.7). Jednak podobnie jak w przypadku Postgres, nie musimy ponownie tworzyć archiwów [personnes-dao.jar] i [personnes-service.jar], ponieważ nie wprowadziliśmy żadnych zmian w klasach.
Wdrażamy projekt internetowy [mvc-personnes-05B] w środowisku Tomcat:
![]() | ![]() |
Uruchomiono SGBD i MySQL. 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-05B]:

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

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







