JavaEE 环境与程序设计 · 交互课程
技术演进接力链(M1 → M9)
  1. 1WEB 基础
    HTTP / Browser / WebServer
    静态页面写死,人人看到一样
  2. 2Servlet
    手写 if-else 分发不可扩展,无生命周期/并发管理
  3. 3JSP:参数散字符串
    参数散成一堆 String,取值靠字段名硬记
  4. 3JSP:scriptlet 塞逻辑
    scriptlet 把逻辑塞进页面,代码与 HTML 糊在一起
  5. 3JavaBean:数据对象化
    对象全靠 new、层层硬依赖
  6. 4Spring IoC
    Web 路由仍手写 web.xml/if-else、手动取参绑定,展示/业务/数据访问没分开
  7. 5MVC · SpringMVC
    数据在库里,访问是重复样板 + 拼串有注入风险
  8. 6MySQL
    Java 连库要手写五步,连接与资源管理全手动
  9. 7JDBC
    SQL 与 Java 混写、结果集逐行手映射、没有动态 SQL
  10. 8MyBatis
    四份配置 + web.xml 装配繁琐,部署要外部 Tomcat
  11. 9SpringBoot
    链尾 · 不考核

MyBatis

承接上一章的死结
上一章(JDBC)的死结:SQL 与 Java 混写、结果集逐行手映射、没有动态 SQL。本章用 MyBatis「接口 + XML」把重复动作封装掉。

MyBatis

目标:掌握 MyBatis「Mapper 接口 + XML 映射」的持久层开发,理解从 DAO 方法调用到结果集映射的全链路,能写出动态 SQL 与 resultMap。

本模块引入 ORM 框架 MyBatis:用 @Mapper 接口写 DAO、用 XML 写 SQL 与结果映射,由 MyBatis 生成动态代理对象把接口方法执行成真实的数据库操作。以真实工程 GuestBookSSM 为样板(WordDao + WordMapper.xml + mybatis-config.xml)。

一、MyBatis 是什么

对照 GuestBookSSM:WordDao(@Repository @Mapper 接口)+ WordMapper.xml(SQL + resultMap)+ mybatis-config.xml(typeAliases、注册映射文件)。

二、一次查询的六步全链路

调用 wordDao.selectById(id),MyBatis 内部要走过这样一条链:

call → proxy → locate → bind → execute → map

  1. call:业务代码调用 Mapper 接口方法;
  2. proxy:MyBatis 动态代理拦截这次调用;
  3. locate:按 namespace + 方法名 定位到 XML 里对应的 <select id>;
  4. bind:把 #{} 参数绑定进 PreparedStatement;
  5. execute:执行 SQL,得到 ResultSet;
  6. map:按 resultMap/resultType 把行数据映射成对象。

下面交互实验逐步看这条链(对照 GuestBookSSM 的 WordMapper.xml):

交互实验:MyBatis 一次查询全链路(MyBatisMapperFlow)

先选一个 DAO 方法,再点「下一步」,看一次 wordDao.findAll() 如何从接口调用一路走到 Java 对象。对照真实工程 GuestBookSSM(WordDao + WordMapper.xml)。

调用 DAO 接口
→
动态代理拦截
→
namespace+id 定位 SQL
→
参数映射
→
执行 SQL
→
结果映射成对象

调用 DAO 接口 —— 负责组件 WordDao.findAll()

应用层像调普通 Java 方法一样调 wordDao.findAll()——接口里没有实现类,却能返回数据

#{...}:预编译占位符,MyBatis 把它转成 PreparedStatement 的 ?,参数与 SQL 分离,天然防 SQL 注入;${...}:字符串直接拼接进 SQL,用于动态表名/列名等无法预编译的场景,但必须校验输入,否则有注入风险。

底层走的仍是 JDBC:MyBatis 只是把「拼 SQL、绑参数、执行、取结果」这些重复动作封装好了。想看清 JDBC 本身,见『JDBC』章(模块7):

交互实验:JDBC 五步时序图(JDBCSequenceDiagram)

接口 · 多态

JDBC 里 Driver / Connection / Statement / ResultSet 都是接口,MySQL 驱动提供具体实现。 下面按「加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 结果与关闭」逐步高亮每条消息, 看调用如何在各对象之间流动、运行期如何落到具体实现。

① 加载驱动② 建立连接③ 创建语句④ 执行 SQL⑤ 结果与关闭
第 1 / 12 步 · ① 加载驱动
Class.forName("com.mysql.jdbc.Driver")—— 触发驱动类的 static 块,把自身登记进 DriverManager
应用程序main()DriverManager静态工厂Driver接口 · com.mysql.jdbc.DriverConnection接口 · ConnectionImplStatement接口 · StatementImplResultSet接口 · ResultSetImplClass.forName

接口与具体实现(多态示例)

接口MySQL 驱动实现作用
Drivercom.mysql.jdbc.Driver连接数据库的入口,负责自注册
ConnectionConnectionImpl一条到数据库的连接会话
StatementStatementImpl执行 SQL、拿结果集的句柄
ResultSetResultSetImpl查询返回的行集,可逐行遍历

经典 JDBC 五步代码

// ① 加载驱动:触发其 static 块完成自注册
Class.forName("com.mysql.jdbc.Driver");

// ② 建立连接:DriverManager 按 URL 选中具体驱动实现(多态)
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mytest", "root", "");

// ③ 创建语句
Statement stmt = conn.createStatement();

// ④ 执行 SQL
stmt.executeUpdate("insert into guestbook(username,title) values('tom','from tom')");
ResultSet rs = stmt.executeQuery("select * from guestbook");

// ⑤ 处理结果 + 关闭(顺序与创建相反)
while (rs.next()) {
    System.out.println(rs.getInt("id") + " " + rs.getString("username"));
}
rs.close();
stmt.close();
conn.close();

JDBC / 多态要点

  • JDBC 的核心是接口:Driver / Connection / Statement / ResultSet 都是接口,厂商提供具体实现——编译期依赖接口、运行期由 DriverManager 按 URL 选中实现,正是模块2 学的多态落到真实 API;
  • 五步时序:加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 处理结果并关闭;资源释放顺序与创建相反(先 rs/stmt,再 conn);
  • Class.forName 只负责触发驱动类的 static 块完成自注册;JDBC 4.0 起可由 SPI 自动发现而省略,但显式写出更稳妥、便于定位问题;
  • 拼接 SQL 字符串有 SQL 注入风险:应使用 PreparedStatement 预编译 + 占位符 ?,把数据与命令分离(见下一节)。

三、# 与 $:两种占位符

GuestBookSSM 的查询全部用 #{},例如 where g.id = #{id}。

四、动态 SQL:<if> / <where> / <foreach>

条件不固定时,用动态 SQL 标签按条件拼出最终 SQL:

下面交互实验实时看拼接效果(对照留言搜索场景):

交互实验:动态 SQL 实时拼接(DynamicSQLExplorer)

开关下面的条件,看 <where> / <if> 如何实时拼出最终 SQL;再增删批量插入的行,看 <foreach> 如何生成 values。

搜索条件:
select g.id,g.title,g.contents,u.username
from guestbook g, users u where g.userid=u.id
where g.title like concat('%', #{title}, '%')
批量插入(3 行):
insert into guestbook (title, contents) values
(#{w.title}, #{w.contents}), (#{w.title}, #{w.contents}), (#{w.title}, #{w.contents})
<if test="...">:条件成立才拼入对应 SQL 片段
<where>:自动加 WHERE,并去掉首个条件前多余的 AND/OR
<foreach collection item separator>:对集合逐项生成片段,用 separator 连接(批量插入常用)

五、结果映射:resultType vs resultMap

GuestBookSSM 的 WordMapper.xml 里用 WordUser resultMap,把查询出的用户信息映射进关联对象 user:

交互实验:列 ↔ 属性映射(ResultMapVisualizer)

点击左侧任一数据库列,看它经 resultMap 落到哪个 Java 属性。对照真实工程 GuestBookSSM 的 WordMapper.xml(WordUser resultMap + association)。

idusernametitlecontentsuidusernameWord(主对象)idusernametitlecontentsWord.user(association)user.iduser.username
点击左侧列查看映射说明。

resultType 自动映射:下划线 → 小驼峰(mapUnderscoreToCamelCase)

user_name → userNamecreate_time → createTimeid → id

resultType:列名与属性名一致时自动映射,无需逐条声明(可开 mapUnderscoreToCamelCase 处理下划线↔驼峰);resultMap:显式声明每条 column→property,用于列名≠属性名、以及 association/collection 关联对象。

思考题

  1. MyBatis 为什么能用「接口方法 + XML SQL」的方式写 DAO?动态代理在其中起什么作用?
  2. #{} 和 ${} 的区别是什么?为什么优先用 #{}?
  3. <where> 标签解决了手写动态 SQL 的什么问题?
  4. resultType 与 resultMap 各适用什么场景?<association> 用来做什么?
  5. 一次 Mapper 方法调用,从 call 到 map 中间经历了哪几步?

学习小测验

学习小测验

Q1

MyBatis 中 DAO 接口方法能直接执行 SQL,靠的是( )

Q2

#{} 占位符的特点是( )

Q3

"${}" 占位符适合用于( )

Q4

<where> 标签的作用是( )

Q5

<foreach> 标签常用于( )

Q6

resultType 与 resultMap 的区别是( )

Q7

resultMap 里的 <association> 用来( )

Q8

一次 Mapper 方法调用的正确顺序是( )

本章留下的死结
四份配置 + web.xml 装配繁琐、部署要外部 Tomcat(下一章 SpringBoot)。