Skip to content

19. Webanwendung MVC in einer 3-Tier-Architektur – Beispiel 5, MySQL

19.1. Die Datenbank MySQL

In dieser Version werden wir die Personenliste in einer Datenbanktabelle MySQL 4.x speichern. Wir haben das Paket [Apache – MySQL – PHP] verwendet, das unter der URL [http://www.easyphp.org] verfügbar ist. Die folgenden Screenshots stammen aus dem Client EMS, MySQL, Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager], einem kostenlosen Verwaltungsprogramm für SGBD und MySQL.

Die Datenbank heißt [dbpersonnes]. Sie enthält eine Tabelle namens [PERSONNES]:

Image

Die Tabelle [PERSONNES] enthält die Liste der von der Webanwendung verwalteten Personen. Sie wurde mit den folgenden Befehlen SQL erstellt:

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 scheint weniger umfangreich zu sein als die beiden vorherigen SGBD. Ich konnte keine Einschränkungen (Prüfungen) für die Tabelle festlegen.

  • Zeile 10: Die Tabelle muss den Typ [InnoDB] haben und nicht den Typ [MyISAM], der keine Transaktionen unterstützt.
  • Zeile 2: Der Primärschlüssel hat den Typ auto_increment. Wenn eine Zeile ohne Wert für die Spalte ID der Tabelle eingefügt wird, generiert MySQL automatisch eine Ganzzahl für diese Spalte. Dadurch müssen wir die Primärschlüssel nicht selbst generieren.

Die Tabelle [PERSONNES] könnte folgenden Inhalt haben:

Image

Wir wissen, dass beim Einfügen eines Objekts [Personne] durch unsere Schicht [dao] das Feld [id] dieses Objekts vor dem Einfügen den Wert -1 hat und danach einen anderen Wert als -1 annimmt, wobei dieser Wert der Primärschlüssel ist, der der neu in die Tabelle [PERSONNES] eingefügten Zeile zugewiesen wurde. Schauen wir uns anhand eines Beispiels an, wie wir diesen Wert ermitteln können.

Der Auftrag SQL

SELECT LAST_INSERT_ID()

ermöglicht es, den zuletzt in das Feld ID der Tabelle eingefügten Wert zu ermitteln. Sie ist nach dem Einfügen auszuführen. Dies unterscheidet sich von den Befehlen SGBD, [Firebird] und [Postgres], bei denen der Wert des Primärschlüssels der hinzugefügten Person vor dem Einfügen abgefragt wurde. Wir werden sie in der Datei [personnes-mysql.xml] verwenden, die die in der Datenbank generierten Befehle SQL zusammenfasst.

19.2. Das Eclipse-Projekt der Schichten [dao] und [service]

Um die Schichten [dao] und [service] unserer Anwendung mit der Datenbank MySQL zu entwickeln, verwenden wir das folgende Eclipse-Projekt [mvc-personnes-05]:

Image

Das Projekt ist ein einfaches Java-Projekt, kein Tomcat-Webprojekt.


Ordner [src]


Dieser Ordner enthält den Quellcode der Schichten [dao] und [service] sowie die Konfigurationsdateien dieser beiden Schichten:

Image

Alle Dateien, deren Name „[mysql]“ enthält, wurden möglicherweise gegenüber den Firebird- und Postgres-Versionen geändert – oder auch nicht. Im Folgenden beschreiben wir diejenigen, die geändert wurden.


Ordner [database]


Dieser Ordner enthält das Skript zur Erstellung der Personendatenbank „MySQL“:

Image

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


SET FOREIGN_KEY_CHECKS=0;

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

USE `dbpersonnes`;

#
# Struktur der Tabelle `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;

#
# Daten für die Tabelle `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;

Ordner [lib]


Dieser Ordner enthält die für die Anwendung erforderlichen Archive:

Zu beachten ist das Vorhandensein des JDBC-Treibers SGBD MySQL. Alle diese Archive sind Teil des Classpath des Eclipse-Projekts.

19.3. Die Schicht [dao]

Die Schicht [dao] sieht wie folgt aus:

Image

Wir stellen hier nur die Änderungen gegenüber der Version [Firebird] vor.

Die Mapping-Datei [personne-mysql.xml] lautet wie folgt:


<?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 der Klasse [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- Zuordnungstabelle [PERSONNES] – Objekt [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>
    <!-- Liste aller Personen -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- eine bestimmte Person abrufen -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#Wert#</select>
    <!-- eine Person hinzufügen -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#Version#, #Nachname#, #Vorname#, #dateNaissance#, #verheiratet#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- eine Person aktualisieren -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#Version#+1, NOM=#Nachname#, PRENOM=#Vorname#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#Ehepartner#, NBENFANTS=#nbEnfants# WHERE ID=#id# und 
        VERSION=#Version#</update>
    <!-- eine Person löschen -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#Wert# </delete>
    <!-- den Wert des Primärschlüssels [id] der zuletzt eingefügten Person abrufen -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

Der Inhalt entspricht dem von [personnes-firebird.xml], mit folgenden Abweichungen:

  • Der Befehl SQL „Personne.insertOne“ wurde in den Zeilen 29–37 geändert:
  • Der Einfügebefehl SQL wird vor dem Befehl SELECT ausgeführt, wodurch der Wert des Primärschlüssels der eingefügten Zeile abgerufen werden kann
  • Der Einfügebeleg SQL enthält keinen Wert für die Spalte ID der Tabelle [PERSONNES]

Dies spiegelt das Einfügebeispiel wider, das wir in Abschnitt 19.1 erläutert haben.

Es sei angemerkt, dass hier möglicherweise eine Quelle für Probleme zwischen konkurrierenden Threads liegt. Stellen wir uns zwei Threads Th1 und Th2 vor, die gleichzeitig eine Einfügung vornehmen. Insgesamt müssen vier Befehle SQL gesendet werden. Nehmen wir an, sie erfolgen in der folgenden Reihenfolge:

  1. Einfügung I1 durch Th1
  2. Einfügung I2 durch Th2
  3. Abfrage S1 von Th1
  4. Abfrage S2 von Th2

In Schritt 3 ruft Th1 den bei der letzten Einfügung generierten Primärschlüssel ab, also den von Th2 und nicht seinen eigenen. Ich weiß nicht, ob die Methode [insert] von iBATIS für diesen Fall abgesichert ist. Wir gehen davon aus, dass sie dies ordnungsgemäß handhabt. Wäre dies nicht der Fall, müssten wir die Implementierungsklasse [DaoImplCommon] von der Schicht [dao] zu einer Klasse [DaoImplMySQL] ableiten, in der die Methode [insertPersonne] synchronisiert wäre. Dies würde das Problem jedoch nur für die Threads unserer Anwendung lösen. Wenn es sich bei Th1 und Th2 um Threads zweier verschiedener Anwendungen handelt, müsste das Problem sowohl mit Transaktionen als auch mit einem geeigneten Isolationsgrad (Isolation Level) zwischen den Transaktionen gelöst werden. Geeignet wäre die Isolationsstufe [serializable], bei der Transaktionen so ausgeführt werden, als würden sie sequenziell ablaufen.

Es ist zu beachten, dass dieses Problem bei Firebird und Postgres nicht auftritt, da diese SELECT vor INSERT ausführen. Wenn wir beispielsweise folgende Sequenz haben:

  1. select S1 von Th1
  2. select S2 von Th2
  3. Einfügung I1 von Th1
  4. Einfügen von I2 von Th2

In den Schritten 1 und 2 beziehen Th1 und Th2 Primärschlüsselwerte vom selben Generator. Dieser Vorgang ist normalerweise atomar, und Th1 und Th2 erhalten zwei unterschiedliche Werte. Wäre der Vorgang nicht atomar und würden Th1 und Th2 zwei identische Werte abrufen, würde der in Schritt 4 von Th2 durchgeführte Einfügevorgang aufgrund eines doppelten Primärschlüssels fehlschlagen. Dies ist ein vollständig behebbarer Fehler, und Th2 kann den Einfügevorgang erneut versuchen.

Wir lassen den Vorgang „Personne.insertOne“ so, wie er derzeit in der Datei [personnes-mysql.xml] vorliegt, aber der Leser sollte sich bewusst sein, dass hier potenziell ein Problem vorliegt.

Die Implementierungsklasse [DaoImplCommon] der Schicht [dao] entspricht der der beiden vorherigen Versionen.

Die Konfiguration der Schicht [dao] wurde an SGBD und [MySQL] angepasst. Somit lautet die Konfigurationsdatei [spring-config-test-dao-mysql.xml] wie folgt:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- die Datenquelle 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>
    <!-- Zugriffsklasse für die Schicht [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • Zeilen 5–19: Die Bean [dataSource] verweist nun auf die Datenbank [MySQL] [dbpersonnes], deren Administrator [root] ohne Passwort ist. Der Leser sollte diese Konfiguration entsprechend seiner eigenen Umgebung anpassen.
  • Zeile 31: Die Klasse [DaoImplCommon] ist die Implementierungsklasse der Schicht [dao]

Nachdem diese Änderungen vorgenommen wurden, kann mit den Tests begonnen werden.

19.4. Tests der Schichten [dao] und [service]

Die Tests der Schichten [dao] und [service] sind dieselben wie für die Version [Firebird]. Die erzielten Ergebnisse lauten wie folgt:

Es ist festzustellen, dass die Tests mit der Implementierung [DaoImplCommon] erfolgreich bestanden wurden. Wir müssen diese Klasse nicht ableiten, wie es bei den Versionen SGBD und [Firebird] erforderlich war.

19.5. Tests der Anwendung [web]

Um die Webanwendung mit den Implementierungen SGBD und [MySQL] zu testen, erstellen wir ein Eclipse-Projekt [mvc-personnes-05B] analog zu dem Verfahren, das zum Erstellen des Projekts [mvc-personnes-03B] mit der Firebird-Datenbank verwendet wurde (siehe Abschnitt 17.7). Da wir jedoch, wie bei Postgres, keine Klassen geändert haben, müssen wir die Archive [personnes-dao.jar] und [personnes-service.jar] nicht neu erstellen.

Wir stellen das Webprojekt [mvc-personnes-05B] in Tomcat bereit:

SGBD und MySQL werden gestartet. Der Inhalt der Tabelle [PERSONNES] sieht dann wie folgt aus:

Image

Anschließend wird Tomcat gestartet. Mit einem Browser rufen wir die URL [http://localhost:8080/mvc-personnes-05B] auf:

Image

Wir fügen eine neue Person über den Link [Ajout] hinzu:

Wir überprüfen den Eintrag in der Datenbank:

Image

Der Leser wird gebeten, weitere Tests durchzuführen: [modification, suppression].