1.1 从 JDBC 到 SQL 映射手术台 本节摘要:MyBatis 是一款半自动化 SQL 映射框架:SQL 由开发者书写,参数装配与结果组装由框架完成。本节用一段 JDBC 原生代码与等价的 MyBatis 写法逐行对照,数清框架替你消掉的重复劳动,并给出贯穿全册的"手术三段论"模型——参数入、动态拼装、结果出。 这是全册的起点:先看清要被替代的东西长什么样,后面每一章的机制才有着力点。 一台老式手术:JDBC 查询的六步 下面是一段再普通不过的原生 JDBC 代码,按用户名查一条用户记录。它来自大多数教材的第一课,也几乎原样出现在无数老项目的工具类里。 四十多行代码,与业务真正相关的只有两处:第 2 步的 SQL 文本,和第 3 步的参数值。
本节摘要:MyBatis 是一款半自动化 SQL 映射框架:SQL 由开发者书写,参数装配与结果组装由框架完成。本节用一段 JDBC 原生代码与等价的 MyBatis 写法逐行对照,数清框架替你消掉的重复劳动,并给出贯穿全册的"手术三段论"模型——参数入、动态拼装、结果出。
这是全册的起点:先看清要被替代的东西长什么样,后面每一章的机制才有着力点。
下面是一段再普通不过的原生 JDBC 代码,按用户名查一条用户记录。它来自大多数教材的第一课,也几乎原样出现在无数老项目的工具类里。
// 原生 JDBC:按用户名查询一条用户 Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { // 第 1 步:拿到连接(背后还有驱动注册、连接池等前置) conn = DriverManager.getConnection(url, user, password); // 第 2 步:预编译 SQL,参数用问号占位 ps = conn.prepareStatement("SELECT id, username, email FROM users WHERE username = ?"); // 第 3 步:手工装配参数,位置和类型都要自己盯 ps.setString(1, "zhangsan"); // 第 4 步:执行 rs = ps.executeQuery(); // 第 5 步:手工搬运结果集到对象 if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setEmail(rs.getString("email")); System.out.println(u); // 输出:User{id=1, username='zhangsan', email='z@ex.com'} } } catch (SQLException e) { e.printStackTrace(); } finally { // 第 6 步:按依赖顺序反向关闭三个资源,一个都不能漏 try { if (rs != null) rs.close(); } catch (SQLException ignore) {} try { if (ps != null) ps.close(); } catch (SQLException ignore) {} try { if (conn != null) conn.close(); } catch (SQLException ignore) {} }
四十多行代码,与业务真正相关的只有两处:第 2 步的 SQL 文本,和第 3 步的参数值。其余全是"每个查询都要重抄一遍"的仪式性代码。更麻烦的是,这些仪式没有一处可以省略——顺序关错资源、参数位置填错、结果集列名拼错,都会变成运行期才爆的故障。
把这段代码的痛点列成清单,正好对应持久层框架要解决的四个问题:
| JDBC 痛点 | 具体表现 | MyBatis 的对策 |
|---|---|---|
| 资源管理冗长 | 每个查询六步,关闭顺序不能错 | SqlSession 生命周期托管,try-with-resources 一行收尾 |
| 参数装配手工化 | 位置下标与类型全靠人肉对齐 | 井号占位符按名取值,TypeHandler 自动转换 |
| 结果搬运体力活 | 每列一次 getXxx 并 set 进对象 | 结果映射自动把列装进属性 |
| SQL 混在 Java 里 | 改一句 SQL 要重新编译发布 | SQL 集中在 XML 映射文件中维护 |
同样的查询,在 MyBatis 里被拆成两块。先是接口——注意,它没有实现类:
// Mapper 接口:只声明方法,不需要写实现 public interface UserMapper { // 方法名与 XML 中语句 id 一一对应 User findByUsername(String username); }
然后是 XML 映射文件,SQL 就住在这里:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.UserMapper"> <!-- id 对应接口方法名,resultType 指明结果装进哪个类 --> <select id="findByUsername" parameterType="string" resultType="com.example.entity.User"> SELECT id, username, email FROM users WHERE username = #{username} </select> </mapper>
调用方只需要三行:
try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User u = mapper.findByUsername("zhangsan"); // 控制台输出:User{id=1, username='zhangsan', email='z@ex.com'} System.out.println(u); }
对照着数:JDBC 六步里,第 1 步交给 openSession,第 3 步交给井号占位符加 TypeHandler,第 5 步交给 resultType 指向的自动映射,第 6 步交给 try-with-resources。剩下的只有 SQL 本身——这正是"半自动"的含义:框架不生成 SQL、不猜方言、不替你优化语句;它只负责把写好的 SQL 与 Java 方法缝合起来。
初学者常见的失望来自期望错位。下面三件事 MyBatis 不做,需要你自己负责:
这不是缺陷,是取舍。写 SQL 的主动权换来的是对每条语句的完全控制——这也是金融、报表、复杂查询密集的系统长期选择它的原因。反过来的场景,比如纯单表增删改查占九成的原型项目,全自动方案确实更快。
从本节开始,我们把每个映射方法都当成一台手术,拆成固定三段:

三段论不只是一个比喻,它也是排错的检查顺序:参数没进对(值错、名字对不上)、拼装不干净(语法错、条件漏)、结果没收好(字段 null、对象重复),几乎覆盖了 MyBatis 日常故障的全部。
变式练习:把本节的查询改成按邮箱查,JDBC 版与 MyBatis 版各写一遍,数一数你各写了几行、哪几行是抄来的。这个体感差异,就是后面所有章节要展开的空间。
下一节先把手术台搭起来:依赖、配置、第一条可运行的查询,以及首跑最容易撞上的三类报错。