MYBATIS 3.5 · MAPPER · SQLSESSION · EXECUTOR
一行 Java,怎么变成 SQL,又变回对象
你只写了一个接口方法和一段 SQL。中间那些活:找到 SQL、填参数、发给数据库、把一行行结果变成对象,全是 MyBatis 替你干的。
orderMapper.selectById(7)SELECT id, order_no, amount
FROM t_order
WHERE id = ?? ← 7| id | order_no | amount |
|---|---|---|
| 7 | NO20261105 | 99.00 |
Order{id=7, orderNo="NO20261105", amount=99.00}MapperProxy你注入的 Mapper 接口,背后是它。拿接口全名 + 方法名去找 SQL。
JDK 动态代理Configuration启动时把 XML 全读进来,每条 SQL 一页,叫 MappedStatement。
启动时编好,运行时只查Executor管缓存:先翻缓存,没有才去数据库。
每个 SqlSession 一个StatementHandler 等准备语句、填问号、拆结果。最后干活的都是 JDBC。
Statement · Parameter · ResultSet Handler启动时:先把手册编好
MyBatis 启动时把配置和每个 Mapper XML 读一遍,一条 SQL 变成手册里的一页。运行时不再读 XML,只按编号翻页。
<mapper namespace="com.shop.mapper.OrderMapper">
<select id="selectById" resultType="Order">
SELECT id, order_no, amount
FROM t_order
WHERE id = #{id}
</select>
</mapper>
- 编号
com.shop.mapper.OrderMapper.selectById- 类型
- SELECT
- SQL
- RawSqlSource:没有动态标签,
#{id}启动时就换成了? - 结果
- 按
Order自动映射 - 语句
- PREPARED,也就是 PreparedStatement
- 缓存
- useCache=true,flushCache=false(select 的默认值)
namespace + id。运行时替身拿“接口全名 + 方法名”来翻页,两边必须一字不差。对不上就是那句 Invalid bound statement (not found)。MybatisAutoConfiguration 读 mybatis.* 配置,造好 SqlSessionFactory 和 SqlSessionTemplate。就是第 03 篇讲的自动配置。
@MapperScan 或 @Mapper 把每个接口登记成 MapperFactoryBean,你注入拿到的就是 MapperProxy。
mybatis.mapper-locations 指定的路径;或者放在和接口同一个包路径下,登记接口时会顺手加载。
运行时:一次查询经过的人
从上往下一层层交下去,结果再一层层交回来。
MapperProxy → MapperMethodsqlSession.selectOne("…selectById", 7)getBoundSql · createCacheKeyCachingExecutorBaseExecutor.localCacheStatementHandlerParameterHandlerps.execute()ResultSetHandler- orderMapper.selectById(7)其实是 MapperProxy
- MapperProxy.invoke → MapperMethod.execute翻手册
- SqlSessionTemplate.selectOneSpring 里才有这层
- DefaultSqlSession.selectOne → selectList会话
- CachingExecutor.query二级缓存
- BaseExecutor.query → queryFromDatabase一级缓存
- SimpleExecutor.doQuery默认执行器
- PreparedStatementHandler.queryJDBC
- PreparedStatement.execute()数据库
跟着一次调用走一遍
选一个剧情,一步一步看:SQL 拼成了什么样,缓存里有什么,数据库被查了几次,最后返回了什么。
- 1替身翻手册
MapperProxy → MapperMethod - 2会话
SqlSessionTemplate → DefaultSqlSession - Executor 里面
- 3拼 SQL
getBoundSql + CacheKey - 4二级缓存
CachingExecutor - 5一级缓存
BaseExecutor.localCache - StatementHandler 里面(JDBC)
- 6准备语句
prepareStatement - 7填问号
ParameterHandler + TypeHandler - 8执行
ps.execute() - 9处理结果
ResultSetHandler · KeyGenerator - 10回程
放缓存,返回
剧情里 OrderMapper.xml 没写 <cache/>,也就是没开二级缓存,这是最常见的配置。
SQL · BoundSql
会话和缓存
返回给你的
MapperProxy.invoke拼 SQL:动态标签,#{} 和 ${}
第 3 步“拼 SQL”有两件事:先按参数把动态标签算一遍,再把 #{} 换成问号。
<select id="selectByCond" resultType="Order">
SELECT id, order_no, amount FROM t_order
<where>
<if test="status != null">
AND status = #{status}
</if>
<if test="keyword != null and keyword != ''">
AND order_no LIKE CONCAT('%', #{keyword}, '%')
</if>
</where>
</select>
| status | keyword | 拼出来的 WHERE |
|---|---|---|
| 1 | null | WHERE status = ? |
| null | "1105" | WHERE order_no LIKE CONCAT('%', ?, '%')开头的 AND 被 <where> 去掉了 |
| 1 | "1105" | WHERE status = ? AND order_no LIKE … |
| null | null | 没有 WHERE,整张表都查出来 |
WHERE name = #{name}
输入原样当成一个字符串值,查不到人,也改不了 SQL。
WHERE name = '${name}'
#{} 变成问号,用 PreparedStatement 传值;${} 是直接把字符串拼进 SQL。表名、ORDER BY 的列名这类不能用问号的地方才用 ${},而且只接受白名单里的值。
两级缓存
查询顺序是先二级、再一级、最后数据库。一级默认开着,二级要自己在 XML 里写 <cache/>。
OrderMapper 命名空间写了 <cache/> 才有。所有 SqlSession 共用,会话提交或关闭后,结果才放进来。| 一级缓存 | 二级缓存 | |
|---|---|---|
| 默认 | 开着 | 关着,要在 Mapper XML 里写 <cache/> |
| 范围 | 一个 SqlSession | 一个 namespace,跨 SqlSession |
| 什么时候放进去 | 查完马上放 | SqlSession 提交或关闭之后 |
| 什么时候清空 | 本会话任何增删改、commit、rollback、close | 本 namespace 的增删改 |
| 在 Spring 里 | 没开事务时每次调用都是新会话,基本用不上 | 多台机器各存各的;别的 namespace 改了同一张表不会清,容易读到脏数据 |
| 要求 | 无 | 实体要实现 Serializable,默认返回的是序列化出来的副本 |
实际项目里,二级缓存很少开。要缓存,一般在 Service 层用 Redis 自己管。
插件:能插在哪四个地方
MyBatis 只允许拦这四个对象。插件就是给它们套一层 JDK 动态代理,方法被调用时先经过你。
query · update · commit · rollback例:PageHelper 分页,在这里改写 SQL、先查总数。
prepare · parameterize · query · update例:慢 SQL 统计、多租户自动加条件。
setParameters例:入库前给手机号加密。
handleResultSets例:查出来后给身份证号打码。
@Intercepts(@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}))
public class SlowSqlPlugin implements Interceptor {
@Override
public Object intercept(Invocation inv) throws Throwable {
long start = System.currentTimeMillis();
try {
return inv.proceed(); // 继续往下走
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 500) log.warn("慢 SQL {} ms", cost);
}
}
}
- 在 Spring Boot 里声明成
@Bean就行,自动配置会把所有 Interceptor 装进去。 - 多个插件一层套一层:后登记的在最外层,最先执行。
- 插件拿到的是真实对象的方法调用,改错了会影响每一条 SQL。写之前先想清楚要拦哪个方法。
JDK 8 和 JDK 17:流程一样,依赖版本不同
MyBatis 本体两边都是 3.5.x,上面讲的流程完全一样。变的是和 Spring Boot 对接的那几个包。
Spring Boot 2
JDK 8+mybatis-spring-boot-starter2.3.x(配 Boot 2.7),里面是 mybatis-spring 2.1.x- MyBatis-Plus 用
mybatis-plus-boot-starter - 注解里写长 SQL,只能用字符串拼接:
@Select("SELECT id, order_no, amount " +
"FROM t_order " +
"WHERE status = #{status}")
List<Order> selectByStatus(int status);
Spring Boot 3
JDK 17+mybatis-spring-boot-starter3.0.x,里面是 mybatis-spring 3.0.x,最低要 JDK 17- MyBatis-Plus 要换成
mybatis-plus-spring-boot3-starter,用老的会启动失败 - 自动配置登记在
AutoConfiguration.imports里(第 03 篇) - 注解里的 SQL 可以用文本块,好读多了:
@Select("""
SELECT id, order_no, amount
FROM t_order
WHERE status = #{status}
""")
List<Order> selectByStatus(int status);
常踩的坑
先想清楚问题出在哪一步:启动时编手册、拼 SQL、查缓存,还是把行变成对象。
手册里没有这一页。查 mybatis.mapper-locations 的路径;XML 放在 src/main/java 下的话,Maven 默认不打包;再核对 namespace 和 id 跟接口全名、方法名是否一致。
方法有多个参数,没写 @Param,编译时又没带 -parameters,MyBatis 拿不到参数名。给每个参数加上 @Param("status")。
值一律用 #{}。排序列名这类只能用 ${} 的,先和白名单比对。
列名 order_no 和属性名 orderNo 对不上。配 mybatis.configuration.map-underscore-to-camel-case=true,或者写 resultMap。
返回单个对象的方法查出了多行。检查 WHERE 条件,或者把返回类型改成 List。
没事务时每次调用一个新 SqlSession;有事务时,同一会话里第二次查直接拿缓存,看不到别人刚改的数据。不想要它,配 local-cache-scope=statement。
OGNL 把 '' 当成 0 来比,0 != '' 不成立。数字参数只判断 != null。
拼出了 IN ()。调用前先判空,空集合直接返回空结果。
方法返回的是影响行数。配 useGeneratedKeys="true" keyProperty="id",id 会写回你传进去的那个对象。
N+1 问题:<association> / <collection> 用了 select 嵌套查询,每一行再查一次。改成 JOIN 加 resultMap,或者打开懒加载。