Skip to content

第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>
  • &lt;if&gt;:条件拼接。
  • &lt;where&gt;:自动处理首个 AND(智能 WHERE)。
  • &lt;foreach&gt;:遍历集合(IN、批量插入)。
  • &lt;choose&gt;/&lt;when&gt;/&lt;otherwise&gt;:类似 switch。
  • &lt;set&gt;:UPDATE 智能 SET(去末尾逗号)。

动态 SQL 让 MyBatis 灵活应对复杂查询条件,比 JDBC 手拼字符串安全优雅。


📖 五、缓存

一级缓存(SqlSession 级)

默认开启。同一 SqlSession 内,相同查询命中缓存(不查库)。SqlSession close/commit 清空。Spring 中每次请求一个 SqlSession,一级缓存作用有限。

二级缓存(Mapper 级,跨 SqlSession)

需开启(&lt;cache/&gt;)。跨 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 空判断

&lt;if test="name != null"&gt; 对空串和 null 都判断,要 name != null and name != ''


🆚 八、JDBC / MyBatis / JPA 对比

特性JDBCMyBatisJPA/Hibernate
SQL手写全手写 SQL,框架管样板框架生成 SQL
灵活性高(SQL 可控)中(复杂 SQL 难)
样板
适用底层复杂查询、性能敏感标准 CRUD、对象模型

对 C 程序员:MyBatis 像"SQL 模板引擎"——写 SQL 模板+参数,框架处理连接/映射。比 JDBC 少样板,比 JPA 多 SQL 控制。国内互联网公司多用 MyBatis(SQL 可控、性能)。


💡 九、最佳实践

  1. #{} 参数化防注入,${} 慎用(白名单)。
  2. 复杂 SQL 用 XML(动态 SQL、resultMap),简单用注解。
  3. 避免 N+1:JOIN 或批量加载。
  4. 分页用 PageHelper,别手拼 LIMIT。
  5. 生产用 MyBatis-Plus 通用 CRUD + 条件构造器。
  6. 缓存用 Redis,少依赖 MyBatis 二级缓存。
  7. SQL 审查:慢 SQL 监控、索引优化。

📝 练习预告

完成 练习/Ex44_MyBatis.java 中的 6 道题:

  1. Mapper 接口与映射(XML/注解)
  2. #{} vs ${} 防注入
  3. 动态 SQL(if/foreach)
  4. 结果映射(resultMap)
  5. 分页(PageHelper 思路)
  6. 综合:用 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 — 依赖管理、生命周期、多模块