Skip to content

MyBatis 入门实战 ​

Java 中级 · 持久层框架

MyBatis 是「半自动」ORM:SQL 由你写,映射由它做。相比 JPA 全自动,它把 SQL 控制权交还开发者,因此在国内互联网公司几乎是持久层标配。本篇不只会用,更要讲清一次查询从 Mapper 接口到 JDBC 的完整链路和缓存的坑。

一、核心组件与执行链路 ​

组件职责
SqlSessionFactory全局单例,构建 SqlSession
SqlSession一次会话,非线程安全,用完即关
Executor执行器(SIMPLE / REUSE / BATCH),含一二级缓存逻辑
StatementHandler创建/设置 PreparedStatement
ParameterHandler#{} 参数绑定
ResultSetHandler结果集 → 实体映射

1.1 Mapper 接口没有实现类,为什么能调用? ​

MyBatis 用 JDK 动态代理为每个 Mapper 接口生成 MapperProxy。调用接口方法时,代理根据「接口全限定名 + 方法名」找到对应的 MappedStatement,再交给 Executor 执行。

java
// 关键:namespace + id 唯一定位一条 SQL
// namespace = com.demo.mapper.UserMapper
// id        = selectById
// 合起来 = com.demo.mapper.UserMapper.selectById
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User u = mapper.selectById(1L);   // 实际走代理 → Executor

二、#{} 与 ${}:防注入的关键 ​

写法编译结果是否预编译安全性
#{id}? 占位符✅ PreparedStatement安全
${id}字符串直接拼接❌SQL 注入风险
xml
<!-- 正确:占位符,参数安全 -->
<select id="selectById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

<!-- 危险:字符串拼接,${} 会原样塞入 SQL -->
<select id="selectByColumn" resultType="User">
    SELECT * FROM user ORDER BY ${column}   <!-- 排序字段只能这样,但要白名单校验 -->
</select>

为什么 #{} 能防注入

#{} 会生成 ?,由 JDBC 的 PreparedStatement 做参数绑定。SQL 的结构在编译期就已确定,用户输入永远只当「值」而非「SQL 片段」。${} 则把用户输入当 SQL 拼进去,1' OR '1'='1 就能绕过条件。

${} 的合法用途

只有表名、字段名、排序方向这类「SQL 结构」无法用占位符时才用 ${},且必须做白名单校验(如只允许 asc/desc、只允许预定义的列名)。

三、动态 SQL ​

xml
<select id="search" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
    <choose>
        <when test="orderBy != null">ORDER BY ${orderBy}</when>
        <otherwise>ORDER BY id DESC</otherwise>
    </choose>
</select>

<!-- foreach 批量查询 -->
<select id="selectByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
标签作用
<if>条件拼接
<where>智能去掉首个 AND/OR
<set>更新语句智能去掉尾部逗号
<foreach>遍历集合(IN 查询、批量插入)
<choose>/<when>/<otherwise>分支(类似 switch)
<trim>自定义前缀/后缀修剪

四、一级/二级缓存及其坑 ​

缓存作用域默认失效时机
一级缓存SqlSession 内开启SQLSession 关闭/清空、执行 update、commit
二级缓存namespace(Mapper)关闭该 namespace 有任何增删改

一二级缓存的生产坑

  1. 一级缓存:同一 SqlSession 内两次查询返回同一个对象(改一个会影响另一个),且 Spring 整合后每个请求一个 Session,问题被掩盖但存在。
  2. 二级缓存跨 SqlSession 共享,多表关联时一个表更新不会清另一个表的缓存,读到脏数据。
  3. 结论:生产建议关闭二级缓存,用 Redis 做应用级缓存,可控性更强。

五、关联查询:嵌套结果 vs 嵌套查询 ​

xml
<!-- 方式一:嵌套结果(一条 SQL JOIN,推荐,避免 N+1) -->
<resultMap id="userWithOrders" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="orders" ofType="Order">
        <id property="id" column="oid"/>
        <result property="amount" column="amount"/>
    </collection>
</resultMap>

<!-- 方式二:嵌套查询(会触发 N+1) -->
<resultMap id="userWithOrders2" type="User">
    <collection property="orders" column="id"
                select="com.demo.mapper.OrderMapper.selectByUserId"/>
</resultMap>

N+1 查询

<collection select=...> 会对主表每一行再发一条子查询,N 行 → 1+N 条 SQL。数据量大时是性能灾难。用 fetchType="lazy" 或改成 JOIN 的嵌套结果方式。

六、最佳实践 ​

  • 一律用 #{},${} 只在必要时且白名单校验。
  • Mapper 接口 + XML 绑定,namespace 必须等于接口全限定名。
  • 避免 N+1:优先 JOIN 的嵌套结果,或延迟加载 + 批量查询。
  • resultMap 显式映射,比 resultType 自动映射更可控(尤其下划线转驼峰需开 mapUnderscoreToCamelCase)。
  • 批量操作走 ExecutorType.BATCH 或 <foreach>,别在循环里单条 insert。
  • 分页用插件(PageHelper)或 MyBatis-Plus 分页,别手写 LIMIT 到各处。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. Mapper 接口没有实现类却能调用,底层用了什么设计模式?如何定位到具体 SQL?
  2. #{} 与 ${} 的区别是什么?为什么 #{} 能防 SQL 注入?${} 有没有合法场景?
  3. 一级缓存和二级缓存的作用域分别是什么?为什么生产环境建议关闭二级缓存?
  4. 什么是 N+1 查询?用 MyBatis 的两种关联写法各举一例,并说明哪种更好。

本节小结

MyBatis 的执行链是 Mapper 代理 → SqlSession → Executor → StatementHandler → Parameter/ResultSetHandler;#{} 预编译防注入、${} 拼接需白名单;一级缓存 Session 级默认开、二级缓存 namespace 级需手动开且有脏读风险;关联查询警惕 N+1。