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

表 [PERSONNES] 将包含由 Web 应用程序管理的用户列表。该表是通过以下 SQL 命令构建的:
MySQL4.x 似乎比前两个 SGBD 更简陋。我无法为该表设置约束(检查)。
- 第10行:该表必须采用[InnoDB]类型,而非不支持事务的[MyISAM]类型。
- 第2行:主键类型为auto_increment。如果插入的行中未提供表中ID列的值,MySQL会自动为该列生成一个整数。这样可以避免我们自己生成主键。
表 [PERSONNES] 的内容可能如下:

我们知道,当通过我们的 [dao] 层插入 [Personne] 对象时, 该对象的 [id] 字段在插入前等于 -1,插入后则取值不同,该值即为分配给插入到 [PERSONNES] 表中新行的一级主键。让我们通过一个示例来了解如何获取该值。
![]() |
![]() |
订单 SQL
可用于获取表中字段 ID 的最新插入值。该查询应在插入操作完成后执行。 这与 SGBD、[Firebird] 和 [Postgres] 不同,后者是在插入之前查询已添加人员的primary key值。 我们将在文件 [personnes-mysql.xml] 中使用该值,该文件汇总了在数据库上生成的 SQL 命令。
19.2. [dao] 和 [service] 层面的 Eclipse 项目
为了开发基于 MySQL 数据库的应用程序中的 [dao] 和 [service] 层,我们将使用以下 Eclipse 项目 [mvc-personnes-05]:

该项目是一个简单的 Java 项目,并非 Tomcat Web 项目。
[src]文件夹
该文件夹包含 [dao] 和 [service] 层级的源代码,以及这两个层级的配置文件:

所有名称中包含 [mysql] 的文件,在 Firebird 和 Postgres 版本中可能已进行修改,也可能未作修改。下文将描述其中已修改的文件。
文件夹 [database]
该文件夹包含用于创建人员数据库的脚本 MySQL:
![]()
文件夹 [lib]
该文件夹包含应用程序所需的存档文件:
![]() |
值得注意的是,SGBD 包含 jdbc 驱动程序 MySQL。所有这些归档文件均属于 Eclipse 项目的 Classpath。
19.3. [dao] 层
[dao] 层如下:

本文仅介绍与版本 [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 命令。假设它们按以下顺序执行:
- Th1 执行插入操作 I1
- Th2 执行插入操作 I2
- Th1执行查询 S1
- 从 Th2 查询 S2
在步骤 3 中,Th1 获取的是上次插入时生成的主键,即 Th2 的主键,而非其自身的主键。我不确定 iBATIS 的方法 [insert] 是否针对这种情况进行了保护。我们假设它能正确处理这种情况。 如果并非如此,我们需要将 [DaoImplCommon] 实现类从 [dao] 层派生为 [DaoImplMySQL] 类,并在其中对 [insertPersonne] 方法进行同步。 但这仅能解决我们应用程序内部线程的问题。如果上述 Th1 和 Th2 属于两个不同应用程序的线程,则必须同时通过事务以及事务间适当的隔离级别(isolation level)来解决该问题。 此时,应采用 [serializable] 隔离级别,该级别下事务的执行效果等同于顺序执行。
需要注意的是,Firebird 和 Postgres 版本(SGBD)不存在此问题,因为它们会在执行 INSERT 之前先执行 SELECT。例如,如果执行以下语句序列:
- select S1 from Th1
- select S2 来自 Th2
- Th1 的插入操作 I1
- 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] 表的内容如下:

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

我们通过 通过链接 [Ajout] 添加一位新用户:
![]() | ![]() |
我们在数据库中验证新增记录:

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







