Skip to content

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

Image

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:

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

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

Image

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

SELECT LAST_INSERT_ID()

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

Image

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:

Image

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:

Image

# EMS MySQL Manager Lite 3.2.0.1
# ---------------------------------------
# Host      : localhost
# Port:     3306
# Baza danych: dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

CREATE DATABASE `dbpersonnes`
    CHARACTER SET 'latin1'
    COLLATE 'latin1_swedish_ci';

USE `dbpersonnes`;

#
# Struktura tabeli `personnes`: 
#

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

#
# Dane tabeli `personnes` (LIMIT  0,500)
#

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

COMMIT;

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:

Image

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:

  1. wstawienie I1 przez Th1
  2. wstawienie I2 przez Th2
  3. polecenie select S1 wykonane przez wątek Th1
  4. 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ę:

  1. select S1 z Th1
  2. select S2 z Th2
  3. wstawienie I1 z Th1
  4. 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:

Image

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

Image

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

Sprawdzamy, czy wpis został dodany do bazy danych:

Image

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