1. MyBatis 入门:理解 ORM 思想与核心架构
十年前我第一次接触 JDBC 那堆繁琐的 try-catch 和结果集解析时,就梦想着能有种更优雅的数据访问方式。直到遇见 MyBatis,这个半自动化的 ORM 框架完美平衡了 SQL 控制权与开发效率。今天我们就来拆解这个持久层框架的核心机制,我会结合自己踩过的坑,带你掌握从配置到 SQL 映射的完整知识体系。
MyBatis 本质上是个 SQL 模板引擎,它不像 Hibernate 那样完全屏蔽数据库细节,而是让你用 XML 或注解的方式定义 SQL,自动完成参数映射和结果集转换。这种设计特别适合需要精细控制 SQL 的场景,比如互联网业务中那些复杂的多表关联查询。下面这张简图揭示了它的核心工作原理:
![MyBatis 工作流程:配置加载 → SQL 解析 → 参数映射 → SQL 执行 → 结果映射 → 对象返回]
2. 核心配置详解与最佳实践
2.1 配置文件层级解析
mybatis-config.xml 是框架的中枢神经系统,我习惯把它划分为三个功能域:
xml复制<!-- 典型配置结构 -->
<configuration>
<!-- 环境配置域 -->
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 映射文件域 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
<!-- 全局参数域 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
环境配置的坑点:
- 生产环境一定要用 POOLED 数据源,我曾在压测时发现 UNPOOLED 模式导致连接数暴涨
- 事务管理器选型:JDBC 适合简单事务,MANAGED 需要容器支持
2.2 那些容易被忽略的全局参数
这几个参数会显著影响开发体验:
xml复制<settings>
<!-- 下划线转驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 查询超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 批量操作时特别有用 -->
<setting name="jdbcBatchSize" value="1000"/>
</settings>
警告:aggressiveLazyLoading 参数在关联查询时可能引发 N+1 问题,建议保持默认关闭
3. SQL 映射的十八般武艺
3.1 XML 映射文件解剖
UserMapper.xml 的典型结构:
xml复制<mapper namespace="com.example.dao.UserMapper">
<!-- 结果映射 -->
<resultMap id="userMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
<association property="department" select="getDeptById" column="dept_id"/>
</resultMap>
<!-- 基础CRUD -->
<select id="selectById" resultMap="userMap">
SELECT * FROM users WHERE user_id = #{id}
</select>
<!-- 动态SQL -->
<update id="updateSelective">
UPDATE users
<set>
<if test="username != null">user_name=#{username},</if>
<if test="age != null">age=#{age},</if>
</set>
WHERE user_id=#{id}
</update>
</mapper>
结果映射的黄金法则:
- 始终显式定义
标签,否则级联操作可能出问题 - 复杂对象用
和 处理 - 使用 constructor 标签可以映射不可变对象
3.2 动态 SQL 实战技巧
这是我总结的动态 SQL 优先级指南:
| 场景 | 推荐语法 | 性能影响 |
|---|---|---|
| 条件分支 | ★☆☆☆☆ | |
| 多值 IN 查询 | ★★☆☆☆ | |
| 批量更新 | ★★★☆☆ | |
| 复杂条件组合 | ★☆☆☆☆ |
xml复制<!-- 智能 WHERE 子句示例 -->
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%',#{name},'%')
</if>
<if test="roles != null">
AND role_id IN
<foreach item="item" collection="roles"
open="(" separator="," close=")">
#{item}
</foreach>
</if>
</where>
ORDER BY create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
4. 高级特性与性能优化
4.1 插件开发实战
拦截器是扩展 MyBatis 的瑞士军刀。这里展示一个慢查询监控插件:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class})
})
public class SlowQueryPlugin implements Interceptor {
private long threshold = 1000; // 毫秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
if(cost > threshold) {
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(handler);
String sql = metaObject.getValue("delegate.boundSql.sql").toString();
log.warn("Slow query detected: {}ms - {}", cost, sql);
}
return result;
}
}
插件开发注意事项:
- 不要修改 BoundSql 的原始 SQL,可能导致缓存失效
- 批量操作时 intercept 方法会被多次调用
- 使用 MetaObject 可以安全访问私有字段
4.2 二级缓存陷阱指南
MyBatis 的二级缓存有几个深坑需要注意:
- 分布式环境必须实现 Cache 接口使用 Redis 等集中式缓存
- 事务提交后才会更新缓存,可能读到脏数据
- 关联表更新时需要手动配置 cache-ref
- 建议对查询多更新少的表开启缓存
xml复制<!-- 安全使用缓存的配置示例 -->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
<cache-ref namespace="com.example.mapper.DepartmentMapper"/>
5. 生产环境问题排查手册
5.1 常见异常解决方案
| 异常现象 | 可能原因 | 解决方案 |
|---|---|---|
| Invalid bound statement | 映射文件未加载/ID 不匹配 | 检查 mapperLocations 配置 |
| Parameter 'xxx' not found | 参数名与占位符不匹配 | 使用 @Param 注解明确指定 |
| TooManyResultsException | 返回结果多于一行 | 修改 SQL 或使用 List 接收 |
| Transaction timeout | 长事务阻塞 | 调整 defaultStatementTimeout |
5.2 性能优化检查清单
这是我团队内部的 MyBatis 调优 checklist:
-
【必做】启用 prepareStatement 缓存
xml复制<setting name="defaultExecutorType" value="REUSE"/> -
【推荐】大数据量查询使用游标
java复制@Select("SELECT * FROM large_table") @Options(resultSetType = FORWARD_ONLY, fetchSize = 1000) Cursor<LargeData> streamAll(); -
【重要】批量操作使用 BatchExecutor
java复制SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = session.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); } session.commit(); } finally { session.close(); } -
【监控】定期检查慢查询日志
xml复制<setting name="logImpl" value="SLF4J"/>
最后分享一个真实案例:我们曾遇到分页查询越来越慢的问题,最终发现是 MyBatis 缓存了所有历史查询结果。解决方案是在分页 SQL 中添加参数差异化的条件,比如 AND timestamp > #{lastQueryTime},使每个分页请求生成不同的缓存键。