Skip to content

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

19.1. 数据库 MySQL

在此版本中,我们将人员列表导入 MySQL 4.x 数据库表中。我们使用了可在 [http://www.easyphp.org] 网址获取的 [Apache – MySQL – PHP] 软件包。 下文中的屏幕截图来自 EMS、MySQL Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager], 这是 SGBD MySQL 的免费管理客户端。

该数据库名为 [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

MySQL4.x 似乎比前两个 SGBD 更简陋。我无法为该表设置约束(检查)。

  • 第10行:该表必须采用[InnoDB]类型,而非不支持事务的[MyISAM]类型。
  • 第2行:主键类型为auto_increment。如果插入的行中未提供表中ID列的值,MySQL会自动为该列生成一个整数。这样可以避免我们自己生成主键。

表 [PERSONNES] 的内容可能如下:

Image

我们知道,当通过我们的 [dao] 层插入 [Personne] 对象时, 该对象的 [id] 字段在插入前等于 -1,插入后则取值不同,该值即为分配给插入到 [PERSONNES] 表中新行的一级主键。让我们通过一个示例来了解如何获取该值。

订单 SQL

SELECT LAST_INSERT_ID()

可用于获取表中字段 ID 的最新插入值。该查询应在插入操作完成后执行。 这与 SGBD、[Firebird] 和 [Postgres] 不同,后者是在插入之前查询已添加人员的primary key值。 我们将在文件 [personnes-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
# ---------------------------------------
# 主机    localhost
# 端口    3306
# 数据库:dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

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

USE `dbpersonnes`;

#
# `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;

#
# `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;

文件夹 [lib]


该文件夹包含应用程序所需的存档文件:

值得注意的是,SGBD 包含 jdbc 驱动程序 MySQL。所有这些归档文件均属于 Eclipse 项目的 Classpath

19.3. [dao] 层

[dao] 层如下:

Image

本文仅介绍与版本 [Firebird] 相比的变化。

映射文件 [personne-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>
    <!-- 别名类 [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- 映射表 [PERSONNES] - 对象 [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>
    <!-- 所有人员的列表 -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- 获取特定人员 -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#值#</select>
    <!-- 添加人员 -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#版本#, #姓#, #名#, #dateNaissance#, #已婚#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- 更新人员信息 -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#版本#+1, NOM=#姓#, PRENOM=#名#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#玛丽#, NBENFANTS=#nbEnfants# WHERE ID=#id# 以及 
        VERSION=#version#</update>
    <!-- 删除人员 -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
    <!-- 获取最后插入的联系人主键 [id] 的值 -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

其内容与 [personnes-firebird.xml] 相同,仅存在以下细微差异:

  • SQL 中的“Personne.insertOne”指令在第29-37行发生了变更:
  • 插入指令 SQL 在 SELECT 之前执行,后者将用于获取插入行主键的值
  • 插入语句 SQL 未为表 [PERSONNES] 的列 ID 提供值

这反映了我们在 19.1 节中讨论过的插入示例。

需要注意的是,这可能成为并发线程之间产生问题的潜在源头。假设两个线程 Th1 和 Th2 同时进行插入操作。总共需要发出四个 SQL 命令。假设它们按以下顺序执行:

  1. Th1 执行插入操作 I1
  2. Th2 执行插入操作 I2
  3. Th1执行查询 S1
  4. 从 Th2 查询 S2

在步骤 3 中,Th1 获取的是上次插入时生成的主键,即 Th2 的主键,而非其自身的主键。我不确定 iBATIS 的方法 [insert] 是否针对这种情况进行了保护。我们假设它能正确处理这种情况。 如果并非如此,我们需要将 [DaoImplCommon] 实现类从 [dao] 层派生为 [DaoImplMySQL] 类,并在其中对 [insertPersonne] 方法进行同步。 但这仅能解决我们应用程序内部线程的问题。如果上述 Th1 和 Th2 属于两个不同应用程序的线程,则必须同时通过事务以及事务间适当的隔离级别(isolation level)来解决该问题。 此时,应采用 [serializable] 隔离级别,该级别下事务的执行效果等同于顺序执行。

需要注意的是,Firebird 和 Postgres 版本(SGBD)不存在此问题,因为它们会在执行 INSERT 之前先执行 SELECT。例如,如果执行以下语句序列:

  1. select S1 from Th1
  2. select S2 来自 Th2
  3. Th1 的插入操作 I1
  4. Th2 插入 I2

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

我们将保持“Personne.insertOne”操作在文件[personnes-mysql.xml]中的现有状态,但读者需意识到此处可能存在问题。

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

[dao]层的配置已调整为SGBD和[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>
    <!-- 数据源 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>
    <!-- [dao] 层访问类 -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • 第 5-19 行:bean [dataSource] 现指向数据库 [MySQL] [dbpersonnes],其管理员为 [root],且无需密码。 读者应根据自身环境修改此配置。
  • 第 31 行:类 [DaoImplCommon] 是 [dao] 层的实现类

完成这些修改后,即可进行测试。

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

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

测试结果表明,[DaoImplCommon] 实现已成功通过测试。与 SGBD 和 [Firebird] 不同,我们无需对该类进行派生。

19.5. [web] 应用程序测试

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

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

SGBD 和 MySQL 已启动。此时 [PERSONNES] 表的内容如下:

Image

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

Image

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

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

Image

请读者进行其他测试:[modification, suppression]。