Appearance
第44课:MyBatis
🎯 学习目标
- 理解 MyBatis 在 JDBC 上的封装(SQL 映射、对象关系)
- 掌握 Mapper 接口、XML 映射、注解映射
- 掌握动态 SQL(if/foreach/choose/where/set)
- 理解一二级缓存、插件、分页
- 了解 MyBatis-Plus 的增强
📖 一、概念讲解:MyBatis 是什么
1. JDBC 的样板代码问题
JDBC 写数据库访问要大量样板:获取连接、PreparedStatement、设参数、遍历 ResultSet 映射对象、关闭资源。MyBatis 把这些封装,开发者只写 SQL + 映射。
JDBC:手动建连接/语句/结果集/映射对象/关闭(样板多)
MyBatis:写 SQL + 映射,框架处理样板
JPA/Hibernate:连 SQL 都生成(更高级,但失控)MyBatis 是"半自动 ORM"——SQL 自己写(灵活、可控),映射和资源管理框架做。适合复杂 SQL、对性能/SQL 控制要求高的场景。
2. 核心组件
| 组件 | 作用 |
|---|---|
| SqlSessionFactory | 创建 SqlSession(工厂) |
| SqlSession | 执行 SQL、管理事务、获取 Mapper |
| Mapper 接口 | 定义数据访问方法(与 XML 映射) |
| XML 映射文件 | SQL + 结果映射 |
| 结果映射 | ResultSet → 对象 |
📖 二、Mapper 接口 + XML 映射
java
// Mapper 接口(定义方法)
public interface UserMapper {
User selectById(int id);
List<User> selectByName(String name);
int insert(User user);
}xml
<!-- UserMapper.xml(SQL 映射) -->
<mapper namespace="com.example.UserMapper">
<select id="selectById" resultType="User">
SELECT id, name, age FROM users WHERE id = #{id}
</select>
<select id="selectByName" resultType="User">
SELECT * FROM users WHERE name LIKE #{name}
</select>
<insert id="insert">
INSERT INTO users(name, age) VALUES(#{name}, #{age})
</insert>
</mapper>namespace 对应 Mapper 接口全限定名,id 对应方法名,#{param} 参数化(防注入)。
使用:
java
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
}📖 三、注解映射(简单 SQL)
java
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(int id);
@Insert("INSERT INTO users(name,age) VALUES(#{name},#{age})")
int insert(User user);
}简单 SQL 用注解(简洁),复杂 SQL 用 XML(动态 SQL、结果映射)。两者可混用。
📖 四、动态 SQL(MyBatis 杀手锏)
xml
<select id="search" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<choose>
<when test="sort == 'asc'">ORDER BY id ASC</when>
<otherwise>ORDER BY id DESC</otherwise>
</choose>
</where>
</select>
<insert id="batchInsert">
INSERT INTO users(name) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.name})
</foreach>
</insert><if>:条件拼接。<where>:自动处理首个 AND(智能 WHERE)。<foreach>:遍历集合(IN、批量插入)。<choose>/<when>/<otherwise>:类似 switch。<set>:UPDATE 智能 SET(去末尾逗号)。
动态 SQL 让 MyBatis 灵活应对复杂查询条件,比 JDBC 手拼字符串安全优雅。
📖 五、缓存
一级缓存(SqlSession 级)
默认开启。同一 SqlSession 内,相同查询命中缓存(不查库)。SqlSession close/commit 清空。Spring 中每次请求一个 SqlSession,一级缓存作用有限。
二级缓存(Mapper 级,跨 SqlSession)
需开启(<cache/>)。跨 SqlSession 共享。适合读多写少。注意:写操作清缓存。生产多用 Redis 替代二级缓存(分布式、可控)。
📖 六、插件、分页、MyBatis-Plus
- 插件:拦截 SQL 执行(如分页插件 PageHelper 拦截查询自动加 LIMIT)。
- PageHelper:分页插件,
PageHelper.startPage(1,10)后查询自动分页。 - MyBatis-Plus:增强版,提供通用 CRUD(继承 BaseMapper 免写简单 SQL)、条件构造器、代码生成。
java
// MyBatis-Plus:继承 BaseMapper 免写 CRUD
public interface UserMapper extends BaseMapper<User> {
// selectById/insert/updateById 自动有
}⚠️ 七、常见陷阱
陷阱1:${} 注入
#{} 参数化(安全),${} 字符串拼接(注入风险,仅用于表名/列名/排序字段等不能参数化的场景,且必须白名单校验)。
陷阱2:N+1 查询
关联查询用懒加载,查 N 条主记录后逐条查关联(N+1 次 SQL)。用 JOIN 或 fetchType=eager 一次查。
陷阱3:缓存脏数据
二级缓存跨会话,写操作清缓存不及时(分布式场景不一致)。生产用 Redis 显式缓存更可控。
陷阱4:结果映射错误
列名与属性名不一致需 resultMap 映射或别名(SELECT user_name AS userName)。
陷阱5:动态 SQL 空判断
<if test="name != null"> 对空串和 null 都判断,要 name != null and name != ''。
🆚 八、JDBC / MyBatis / JPA 对比
| 特性 | JDBC | MyBatis | JPA/Hibernate |
|---|---|---|---|
| SQL | 手写全 | 手写 SQL,框架管样板 | 框架生成 SQL |
| 灵活性 | 高 | 高(SQL 可控) | 中(复杂 SQL 难) |
| 样板 | 多 | 少 | 少 |
| 适用 | 底层 | 复杂查询、性能敏感 | 标准 CRUD、对象模型 |
对 C 程序员:MyBatis 像"SQL 模板引擎"——写 SQL 模板+参数,框架处理连接/映射。比 JDBC 少样板,比 JPA 多 SQL 控制。国内互联网公司多用 MyBatis(SQL 可控、性能)。
💡 九、最佳实践
- #{} 参数化防注入,
${}慎用(白名单)。 - 复杂 SQL 用 XML(动态 SQL、resultMap),简单用注解。
- 避免 N+1:JOIN 或批量加载。
- 分页用 PageHelper,别手拼 LIMIT。
- 生产用 MyBatis-Plus 通用 CRUD + 条件构造器。
- 缓存用 Redis,少依赖 MyBatis 二级缓存。
- SQL 审查:慢 SQL 监控、索引优化。
📝 练习预告
完成 练习/Ex44_MyBatis.java 中的 6 道题:
- Mapper 接口与映射(XML/注解)
- #{} vs ${} 防注入
- 动态 SQL(if/foreach)
- 结果映射(resultMap)
- 分页(PageHelper 思路)
- 综合:用 Java 模拟动态 SQL 拼接
完成后对比 答案/Sol44.java,查看逐行讲解与多解法。
📖 十、resultMap 与复杂映射
简单字段可以用 resultType,复杂映射应使用 resultMap。
示例:
xml
<resultMap id="UserMap" type="com.example.User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="createdAt" column="created_at"/>
</resultMap>
<select id="selectById" resultMap="UserMap">
SELECT id, user_name, created_at
FROM users
WHERE id = #{id}
</select>使用场景:
text
列名和属性名不一致。
需要映射枚举、时间、JSON 字段。
一对一、一对多关联。
复杂查询结果映射到 DTO。建议:
text
简单查询可以用别名解决。
复杂聚合查询优先映射 DTO,而不是强行映射实体。
关联查询要警惕 N+1。📖 十一、动态 SQL 编写规范
动态 SQL 强大,但容易写乱。
推荐规范:
text
where 条件用 <where>,不要手写 WHERE 1=1。
update 条件用 <set>,让 MyBatis 自动处理逗号。
IN 查询用 <foreach>,并处理空集合。
排序字段用白名单,不直接使用 ${}。
复杂 SQL 用 XML,简单 SQL 才考虑注解。空集合要特别处理:
xml
<select id="selectByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>如果 ids 为空,SQL 会非法。Service 层应提前返回空列表:
java
if (ids == null || ids.isEmpty()) {
return List.of();
}🛡 十二、#{} 与 ${} 的边界
#{} 会变成 JDBC 参数占位符:
xml
WHERE name = #{name}它等价于 PreparedStatement,安全。
${} 是字符串替换:
xml
ORDER BY ${column}它不会防注入,必须白名单。
排序字段安全处理:
java
private static final Set<String> ALLOWED_SORT = Set.of("id", "created_at", "user_name");
public String normalizeSort(String sort) {
return ALLOWED_SORT.contains(sort) ? sort : "id";
}原则:
text
普通值:永远 #{}。
表名、列名、排序方向:只能 ${},但必须白名单。
用户输入不允许直接进入 ${}。🧪 十三、分页与性能
分页常见写法:
sql
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100000深分页问题:
text
OFFSET 越大,数据库跳过的数据越多。
排序字段无索引会导致 filesort。
返回字段太多会增加 IO。优化方向:
text
使用覆盖索引。
只查询必要字段。
深分页改为游标分页。
按主键或时间条件向后翻页。游标分页示例:
sql
SELECT id, name
FROM users
WHERE id > #{lastId}
ORDER BY id
LIMIT #{size}MyBatis 只是执行 SQL,性能仍然由 SQL、索引、数据量和数据库执行计划决定。
📖 十四、Spring 集成中的事务
在 Spring 项目中,MyBatis 通常不手动管理 SqlSession,而是交给 Spring。
典型结构:
text
Controller -> Service(@Transactional) -> Mapper -> Database事务应放在 Service 层:
java
@Transactional
public void transfer(long from, long to, BigDecimal amount) {
accountMapper.decrease(from, amount);
accountMapper.increase(to, amount);
}注意事项:
text
同类内部方法调用不会触发事务代理。
默认只对 RuntimeException 回滚。
事务方法应是 public。
不要在事务中执行耗时网络调用。
查询方法可以使用 readOnly=true。MyBatis 负责 SQL 映射,Spring 负责事务边界,两者职责不要混淆。
🛠 十五、MyBatis 排查清单
常见问题:
text
Mapper XML namespace 与接口全限定名不一致。
SQL id 与方法名不一致。
参数名无法识别,缺少 @Param。
resultMap 字段映射错误。
动态 SQL 生成非法语句。
${} 引入注入风险。
分页插件未生效或重复分页。
二级缓存导致数据不一致。排查建议:
text
开启 SQL 日志,查看最终 SQL 和参数。
使用数据库 explain 查看执行计划。
慢查询先看索引,不先改 Java。
对复杂动态 SQL 写单元测试或集成测试。
对 Mapper 方法保持简单清晰,不塞业务判断。✅ 十六、掌握标准
学完本课后,应能做到:
text
能定义 Mapper 接口和 XML 映射。
能用 resultMap 处理字段名不一致。
能写 if、where、set、foreach 动态 SQL。
能解释 #{} 和 ${} 的安全差异。
能识别 N+1 查询和深分页问题。
能知道事务应放在 Service 层。
能排查 Mapper 绑定失败和 SQL 映射错误。
能判断何时使用 MyBatis-Plus 简化 CRUD。MyBatis 的优势是 SQL 可控。写好 MyBatis 的关键不是 XML 语法,而是 SQL 质量、映射边界和事务边界。
🎓 下一步
- 第45课:Maven — 依赖管理、生命周期、多模块