当我们在Java应用中使用MyBatis执行一条SQL查询时,看似简单的session.selectOne()调用背后,隐藏着一系列精妙的封装和转换过程。作为从业十年的老码农,今天带大家完整走一遍这个流程,看看框架如何把我们的方法调用变成最终的JDBC操作。
先看个典型场景:假设我们要查询用户信息,通常会这样写:
java复制User user = sqlSession.selectOne("com.example.mapper.UserMapper.findById", 1L);
这行代码背后,MyBatis需要完成参数处理、SQL解析、执行计划生成、结果映射等十余个关键步骤。整个过程就像快递配送:你只下了个订单(调用方法),但背后有仓库拣货(SQL解析)、物流运输(JDBC执行)、送货上门(结果映射)等多个环节。
先看MyBatis执行SQL时的核心角色分工:
| 组件 | 职责 | 生命周期 |
|---|---|---|
| SqlSession | 门面接口,提供CRUD方法 | 每次数据库操作创建 |
| Executor | 执行器,调度执行过程 | 随SqlSession创建 |
| StatementHandler | SQL语句处理器 | 每次SQL执行创建 |
| ParameterHandler | 参数处理器 | 每次SQL执行创建 |
| ResultSetHandler | 结果集处理器 | 每次SQL执行创建 |
这些组件通过责任链模式协作,每个环节只关注自己的职责。比如Executor不关心SQL具体内容,只负责协调整个执行流程。
完整调用链如下(以查询为例):
这个过程中最容易被忽视的是执行器类型的影响。MyBatis内置三种执行器:
实际项目中,ReuseExecutor在OLTP场景下能提升20%-30%性能,但需要特别注意事务边界。
当我们传入参数1L时,MyBatis要完成类型转换才能交给JDBC。以这个查询为例:
xml复制<select id="findById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
参数处理流程:
#{id}获取参数名MyBatis内置了上百个TypeHandler,处理常见Java与JDBC类型转换。关键实现技巧:
java复制public class LongTypeHandler implements TypeHandler<Long> {
@Override
public void setParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) {
// 核心就是调用PreparedStatement的对应方法
ps.setLong(i, parameter);
}
}
自定义TypeHandler时需要注意数据库兼容性问题。比如Oracle没有BOOLEAN类型,需要特殊处理。
MyBatis通过动态代理将Mapper接口调用转换为XML配置的SQL。关键步骤:
这个过程中最耗时的往往是动态SQL解析。复杂查询建议:
<sql>片段复用公共部分MyBatis默认使用PreparedStatement,相比Statement有三大优势:
但要注意:MySQL驱动在5.0.5之前对预编译有性能问题,需要配置useServerPrepStmts=true。
查询结果到Java对象的转换可能是MyBatis最复杂的部分。以User对象为例:
这里有个性能关键点:反射元数据缓存。MyBatis会对Class的Field/Method信息做缓存,但复杂的resultMap仍会影响性能。
处理一对多关系时,常见的N+1查询问题:
xml复制<resultMap id="userWithOrders" type="User">
<collection property="orders" select="com.example.mapper.OrderMapper.findByUserId" column="id"/>
</resultMap>
这种配置会导致查询1次用户+N次订单。解决方案:
MyBatis本身不管理事务,而是通过Transaction接口抽象。常见实现:
实际项目中最容易出错的是事务隔离级别。MySQL默认REPEATABLE_READ可能导致幻读,需要根据业务场景调整。
生产环境必用连接池,常见选型对比:
| 连接池 | 特点 | 适用场景 |
|---|---|---|
| HikariCP | 高性能 | 高并发OLTP |
| Druid | 监控全面 | 需要审计的场景 |
| Tomcat JDBC | 简单稳定 | 嵌入式应用 |
配置建议:
SELECT 1MyBatis有两级缓存:
缓存使用要点:
推荐监控指标:
诊断慢查询时,先确认是数据库执行慢还是MyBatis处理慢。可以通过日志级别调整:
xml复制<logger name="org.mybatis" level="DEBUG"/>
<logger name="java.sql" level="DEBUG"/>
错误示范:
java复制List<User> users = sqlSession.selectList("findAll");
// 内存中分页
List<User> page = users.subList(start, end);
正确做法:
xml复制<select id="findByPage" resultType="User">
SELECT * FROM user LIMIT #{offset}, #{size}
</select>
低效做法:
java复制for (User user : users) {
mapper.insert(user);
}
高效方案:
java复制// 使用BatchExecutor
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = batchSession.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
batchSession.commit();
}
复杂动态SQL可能导致解析耗时增加。实测案例:
建议对复杂查询进行拆分或使用存储过程。
通过Interceptor接口可以扩展MyBatis核心行为。比如实现SQL耗时统计:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class})
})
public class SqlCostInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
log.debug("SQL执行耗时: {}ms", cost);
return result;
}
}
实现自定义的LanguageDriver可以扩展SQL语法。比如支持模板:
java复制public class TemplateLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(...) {
String sql = parseTemplate(rawSql, parameters);
// 创建标准SqlSource
}
}
Spring管理事务时,关键配置:
java复制@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
常见坑点:
多数据源场景下的解决方案:
注意事务上下文切换时的连接获取问题。
虽然MyBatis已经非常成熟,但仍有优化空间:
在实际项目中,我们团队基于MyBatis扩展了以下功能:
这些扩展点正是MyBatis设计的精妙之处 - 它提供了足够的灵活性,让我们可以根据业务需求定制各种特性。