Skip to content

19. 三层架构中的 MVC Web 应用程序 – 示例 5,MySQL

19.1. MySQL 数据库

在此版本中,我们将把人员列表存储在 MySQL 4.x 数据库表中。 我们使用了可在 [http://www.easyphp.org] 获取的 [Apache – MySQL – PHP] 软件包。下文中的屏幕截图均来自 MySQL Manager Lite 客户端 [http://www.sqlmanager.net/fr/products/mysql/manager],这是一个用于 MySQL 数据库管理系统(DBMS)的免费管理客户端。

该数据库命名为 [dbpersonnes]。其中包含一个名为 [PERSONNES] 的表:

Image

[PERSONNES] 表将存储由 Web 应用程序管理的人员列表。该表是通过以下 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 的功能似乎不如前两个数据库管理系统。我无法向该表添加约束(检查)。

  • 第 10 行:该表必须为 [InnoDB] 类型,而非 [MyISAM],因为后者不支持事务。
  • 第 2 行:主键类型为 auto_increment。如果插入行时未指定表的 ID 列值,MySQL 将自动为该列生成一个整数。这将使我们无需手动生成主键。

[PERSONNES] 表可能包含以下内容:

Image

我们知道,当通过 [DAO] 层插入 [Person] 对象时,该对象的 [id] 字段在插入前等于 -1,插入后则为 -1 以外的值;该值即为插入到 [PERSONNES] 表的新行所分配的主键。让我们通过一个示例来看看如何确定该值。

SQL语句

SELECT LAST_INSERT_ID()

可让我们确定表中 ID 字段插入的最后一个值。该语句必须在插入操作之后执行。这与 [Firebird] 和 [Postgres] 数据库管理系统不同,在后者中,我们是在插入之前就请求了新增人员的primary key值。我们将在 [people-mysql.xml] 文件中使用它,该文件包含在数据库上执行的 SQL 语句。

19.2. 用于 [DAO] 和 [service] 层的 Eclipse 项目

为了使用 MySQL 数据库开发应用程序的 [dao] 和 [service] 层,我们将使用以下 Eclipse 项目 [mvc-personnes-05]:

Image

该项目是一个简单的 Java 项目,而非 Tomcat Web 项目。


[src] 目录


该文件夹包含 [dao] 和 [service] 层的源代码,以及这两个层的配置文件:

Image

所有名称中包含 [mysql] 的文件,其 Firebird 和 Postgres 版本可能已进行修改,也可能未作修改。下面我们将介绍那些已进行修改的文件。


[database] 文件夹


该文件夹包含用于为用户创建 MySQL 数据库的脚本:

Image

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


SET FOREIGN_KEY_CHECKS=0;

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

USE `dbpersonnes`;

#
# Structure for the `personnes` table : 
#

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;

#
# Data for the `personnes` table  (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;

文件夹 [lib]


该目录包含应用程序所需的文件:

请注意 MySQL 数据库管理系统(DBMS)的 JDBC 驱动程序。所有这些文件均属于 Eclipse 项目的类路径

19.3. [dao] 层

[dao] 层如下所示:

Image

我们仅展示与 [Firebird] 版本相比的变更。

映射文件 [person-mysql.xml] 如下所示:


<?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 class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <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>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
    <!-- add a person -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
        VERSION=#version#</update>
    <!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
    <!-- obtain the value of the primary key [id] of the last person inserted -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

这与 [people-firebird.xml] 的内容相同,仅有以下细微差异:

  • 第 29–37 行中的 SQL 语句 "Person.insertOne" 已发生变更:
  • SQL插入语句在SELECT语句之前执行,而SELECT语句用于检索已插入行主键的值
  • SQL 插入语句中未为 [PERSONNES] 表的 ID 列指定值

这与我们在 19.1中讨论的插入示例一致。

请注意,这可能成为并发线程之间问题的潜在根源。设想两个线程 Th1 和 Th2 同时执行插入操作。总共有四条 SQL 语句需要执行。假设它们按以下顺序执行:

  1. Th1 执行插入 I1
  2. Th2 执行 insert I2
  3. Th1 执行 select S1
  4. select S2 由 Th2 执行

在步骤 3 中,Th1 检索了上次插入时生成的主键,该主键属于 Th2 而非其自身。 我不确定 iBATIS 的 [insert] 方法是否能防范这种情况。我们将假设它能正确处理此问题。如果并非如此,我们需要将 [dao] 层中的实现类 [DaoImplCommon] 派生为类 [DaoImplMySQL],并在其中对 [insertPersonne] 方法进行同步。这仅能解决我们应用程序中线程的问题。 如果在上例中,Th1 和 Th2 来自两个不同的应用程序,则需要通过事务以及事务之间的适当隔离级别来解决该问题。此时,[serializable] 级别(事务执行时仿佛是顺序运行的)是合适的。

请注意,Firebird 和 Postgres 数据库管理系统不存在此问题,因为它们会在执行 INSERT 之前先执行 SELECT。例如,考虑以下操作序列:

  1. select S1 from Th1
  2. SELECT S2 from Th2
  3. insert I1 from Th1
  4. insert I2 from Th2

在步骤 1 和 2 中,Th1 和 Th2 从同一个生成器中检索主键值。此操作通常是原子性的,Th1 和 Th2 将检索到两个不同的值。如果该操作不是原子性的,且 Th1 和 Th2 检索到了两个相同的值,那么 Th2 在步骤 4 中执行的插入操作将因主键重复而失败。这是一个完全可恢复的错误,Th2 可以重试插入操作。

我们将保留 [personnes-mysql.xml] 文件中当前的“Personne.insertOne”操作,但读者应注意此处存在潜在问题。

[dao] 层的实现类 [DaoImplCommon] 与前两个版本中的相同。

[dao]层的配置已针对[MySQL]数据库管理系统进行了调整。因此,配置文件[spring-config-test-dao-mysql.xml]如下所示:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source 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>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • 第 5–19 行:[dataSource] Bean 现在指向 [MySQL] 数据库 [dbpersonnes],其管理员为 [root],且无需密码。读者应根据自身环境修改此配置。
  • 第 31 行:[DaoImplCommon] 类是 [dao] 层的实现类

完成这些修改后,我们可以进行测试。

19.4. 针对 [dao] 和 [service] 层的测试

针对 [dao] 和 [service] 层的测试与 [Firebird] 版本相同。所得结果如下:

我们可以看到,使用 [DaoImplCommon] 实现时测试已成功通过。与 [Firebird] 数据库管理系统(DBMS)的情况不同,我们无需派生该类。

19.5. [Web] 应用程序测试

为了使用 [MySQL] 数据库管理系统测试 Web 应用程序,我们以与构建基于 Firebird 数据库的 [mvc-personnes-03B] 项目类似的方式,构建了一个 Eclipse 项目 [mvc-personnes-05B](参见 17.7)。 不过,与 Postgres 一样,由于我们未修改任何类,因此无需重新生成 [personnes-dao.jar] 和 [personnes-service.jar] 归档文件。

我们将 [mvc-personnes-05B] Web 项目部署到 Tomcat 中:

MySQL 数据库管理系统已启动。此时 [PERSONNES] 表的内容如下:

Image

随后启动 Tomcat。使用浏览器访问 URL [http://localhost:8080/mvc-personnes-05B]:

Image

我们通过 [添加]链接添加一名新用户:

我们在数据库中验证新增记录:

Image

欢迎读者进行其他测试 [编辑、删除]。