MyBatis源码与性能优化深度解析:把ORM框架核心讲透,吊打面试官
🎯 写在前面:MyBatis是Java ORM框架的经典之作。相比JPA,MyBatis更加灵活可控;相比JDBC,MyBatis又屏蔽了大量样板代码。但你真的了解MyBatis的底层原理吗?这篇文章,将带你从源码层面深度剖析MyBatis!
一、MyBatis核心架构:一次请求的完整流程
1.1 整体架构图
┌─────────────────────────────────────────────────────────────────────┐
│ MyBatis整体架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ API层(SqlSession) │ │
│ │ SqlSessionFactory → SqlSession → Mapper Proxy │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 核心处理层 │ │
│ │ Executor → StatementHandler → ParameterHandler → ResultSetHandler │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 基础支撑层 │ │
│ │ TransactionManager | ConnectionPool | Cache(一级/二级) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 一次查询的完整流程
// 1. 获取SqlSession(门面入口)
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取Mapper代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 3. 调用接口方法
User user = userMapper.findById(1L);
// 4. 底层原理:jdk动态代理
// MyBatis使用MapperProxyFactory为每个Mapper接口创建代理对象
// 代理对象的invoke方法执行以下逻辑:
// ① 从Configuration中获取MappedStatement
// ② 调用Executor执行SQL
// ③ 处理参数
// ④ 处理结果集
1.3 核心组件详解
/**
* 核心组件及其职责
*/
public class MyBatisCoreComponents {
// 1. SqlSessionFactoryBuilder:构建SqlSessionFactory
// 负责解析mybatis-config.xml和mapper.xml,生成Configuration
// 2. SqlSessionFactory:SqlSession工厂
// 负责创建SqlSession实例
// 3. SqlSession:MyBatis的门面接口
// 提供了所有操作数据库的方法:selectOne, selectList, insert, update, delete
// 4. Executor:执行器(核心)
// - SimpleExecutor:简单执行器
// - ReuseExecutor:复用执行器(缓存Statement)
// - BatchExecutor:批量执行器(批量操作)
// - CachingExecutor:缓存执行器(装饰器模式,包装以上三种)
// 5. StatementHandler:语句处理器
// - PreparedStatementHandler:预编译SQL(防止SQL注入)
// - SimpleStatementHandler:简单SQL
// - CallableStatementHandler:存储过程
// 6. ParameterHandler:参数处理器
// 将Java对象转换为SQL参数
// 7. ResultSetHandler:结果集处理器
// 将ResultSet转换为Java对象
// 8. TypeHandler:类型转换器
// JDBC类型 ↔ Java类型 之间的转换
}
二、SQL执行原理:Executor深度剖析
2.1 Executor家族
┌─────────────────────────────────────────────────────────────────────┐
│ Executor执行器体系 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Executor(接口) │
│ ↑ │
│ ┌───────────┴───────────┐ │
│ │ │ │
│ BaseExecutor CachingExecutor │
│ │ │ │
│ ┌─────────┼─────────┐ ┌──────┴──────┐ │
│ ↓ ↓ ↓ ↓ ↓ │
│ Simple Reuse Batch ←── 包装 ──→ 其他执行器 │
│ Executor Executor Executor │
│ │
│ 职责: │
│ - 管理Statement生命周期 │
│ - 一级缓存管理(BaseExecutor) │
│ - 事务管理 │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 执行流程源码解析
// 核心流程:SqlSession.selectList()
public class SqlSession selectList(String statement, Object parameter) {
// 1. 从Configuration获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 2. 调用Executor执行
return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
// BaseExecutor.query()
public class <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) {
// 1. 创建BoundSql(包含最终SQL和参数)
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 生成缓存Key(一级缓存的key)
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 查询(一级缓存)
List<E> list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
return list;
}
private <E> List<E> queryFromDatabase(...) {
// 1. 先查询一级缓存
List<E> list = localCache.getObject(key);
if (list != null) {
return list; // 缓存命中
}
// 2. 缓存未命中,查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
// 3. 放入一级缓存
localCache.putObject(key, list);
return list;
}
// CachingExecutor.query()(二级缓存)
public <E> List<E> query(MappedStatement ms, Object parameter, ...) {
// 1. 查询二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms); // 根据flushCache配置决定是否清空
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
List<E> list = cache.getObject(key);
if (list != null) {
return list; // 二级缓存命中
}
}
// 2. 调用被包装的执行器查询
List<E> list = delegate.query(ms, parameter, rowBounds, resultHandler, boundSql);
// 3. 放入二级缓存
if (cache != null) {
cache.putObject(key, list);
}
return list;
}
三、缓存机制:二级缓存避坑指南
3.1 一级缓存 vs 二级缓存
┌─────────────────────────────────────────────────────────────────────┐
│ MyBatis缓存机制 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 一级缓存(SqlSession级) │ │
│ │ │ │
│ │ 作用域:SqlSession │ │
│ │ 存储介质: PerpetualCache(HashMap) │ │
│ │ 生命周期: SqlSession关闭后清空 │ │
│ │ 默认开启: 是 │ │
│ │ 所属组件: BaseExecutor │ │
│ │ │ │
│ │ 缓存失效场景: │ │
│ │ ❌ 不同的SqlSession │ │
│ │ ❌ 查询条件不同 │ │
│ │ ❌ 执行了增删改操作(会清空该SqlSession的缓存) │ │
│ │ ❌ 手动调用clearCache() │ │
│ │ ❌ 事务回滚 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 二级缓存(Mapper级) │ │
│ │ │ │
│ │ 作用域:Mapper级别,整个应用共享 │ │
│ │ 存储介质:可配置(PerpetualCache、Ehcache、Redis等) │ │
│ │ 生命周期: 应用运行期间 │ │
│ │ 默认开启: 否(需要配置<cache>) │ │
│ │ 所属组件: CachingExecutor │ │
│ │ │ │
│ │ 特点: │ │
│ │ ✅ 以Mapper为namespace,隔离不同Mapper的缓存 │ │
│ │ ✅ 事务提交后才写入(需要SqlSession提交/close) │ │
│ │ ⚠️ 与一级缓存配合使用 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.2 二级缓存配置与使用
<!-- mapper.xml中开启二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<!--
eviction: 淘汰策略
- LRU: 最近最少使用(默认)
- FIFO: 先进先出
- SOFT: 软引用
- WEAK: 弱引用
flushInterval: 刷新间隔(毫秒),不设置则不刷新
readOnly: 是否只读
size: 缓存对象数量
-->
<cache
eviction="LRU"
flushInterval="60000"
readOnly="false"
size="512"
/>
<!-- 查询结果放入二级缓存 -->
<select id="findById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 增删改操作清空二级缓存 -->
<insert id="insert" useGeneratedKeys="true">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
</mapper>
// 二级缓存与事务配合
@Service
public class UserService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
// 场景1:同一个SqlSession,两次查询(命中一级缓存)
public void queryWithSameSession() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findById(1L); // 查询数据库
User user2 = mapper.findById(1L); // 一级缓存命中
// 两个对象是同一个吗?是的!(同一个SqlSession)
System.out.println(user1 == user2); // true
}
}
// 场景2:不同SqlSession,两次查询(命中二级缓存)
public void queryWithDifferentSessions() {
// 第一个SqlSession
try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
mapper1.findById(1L); // 查询数据库
sqlSession1.commit(); // 重要!提交后才写入二级缓存
}
// 第二个SqlSession
try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
mapper2.findById(1L); // 二级缓存命中
}
}
}
3.3 缓存连环坑
// 坑1:关联查询缓存问题
// userMapper.xml
<select id="findUserWithOrders" resultMap="UserOrderMap">
SELECT u.*, o.* FROM user u
LEFT JOIN orders o ON u.id = o.user_id
</select>
// 问题:如果只更新了order表,userMapper的二级缓存不会失效!
// 解决:使用flushCache="true"强制刷新
<select id="findUserWithOrders" resultMap="UserOrderMap" flushCache="true">
SELECT ...
</select>
// 坑2:缓存与实体修改
public void cacheAndModify() {
User user = mapper.findById(1L); // 缓存到一级缓存
user.setName("newName"); // 修改了user对象
mapper.updateById(user); // 更新数据库
// 但是!一级缓存中的user对象也被修改了!
// 如果后面又查询,返回的是修改后的对象
User user2 = mapper.findById(1L);
System.out.println(user2.getName()); // "newName"
}
四、动态SQL:MyBatis的精髓
4.1 动态SQL标签详解
<!-- 动态SQL核心标签 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- WHERE条件动态拼接 -->
<select id="searchUsers" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE '%' || #{name} || '%'
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
</select>
<!-- choose/when/otherwise(类似switch) -->
<select id="searchByCondition" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="type == 'name'">
name = #{keyword}
</when>
<when test="type == 'email'">
email = #{keyword}
</when>
<otherwise>
id = #{defaultId}
</otherwise>
</choose>
</where>
</select>
<!-- set标签(用于更新) -->
<update id="updateUser">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
<!-- 生成的SQL不会有多余的逗号 -->
</update>
<!-- foreach循环 -->
<select id="findByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
<!-- 生成:WHERE id IN (1, 2, 3, 4, 5) -->
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(name, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
<!-- 生成:INSERT INTO user(name, email) VALUES (?, ?), (?, ?), ... -->
</insert>
<!-- bind标签(绑定变量) -->
<select id="searchByPattern" resultType="User">
<bind name="pattern" value="'%' + namePattern + '%'"/>
SELECT * FROM user WHERE name LIKE #{pattern}
</select>
</mapper>
4.2 动态SQL原理:SqlNode
/**
* 动态SQL解析原理
*
* MyBatis使用SqlNode来表示SQL的各个部分:
* - StaticTextSqlNode:静态文本
* - IfSqlNode:if条件
* - ForeachSqlNode:foreach循环
* - WhereSqlNode:处理WHERE关键字
* - SetSqlNode:处理SET关键字
* - TrimSqlNode:自定义trim
*/
// 解析过程
public class SqlNodeParsing {
// XML中的<if test="...">会被解析为IfSqlNode
public class IfSqlNode implements SqlNode {
private ExpressionEvaluator evaluator;
private String test; // test表达式
private SqlNode contents; // if body
public boolean apply(DynamicContext context) {
// 使用OGNL表达式评估test条件
Boolean result = evaluator.evaluateBoolean(test, context.getBindings());
if (result) {
// 条件为true,应用子节点(拼接SQL片段)
contents.apply(context);
return true;
}
return false;
}
}
// WhereSqlNode自动处理AND/OR前缀
public class WhereSqlNode extends TrimSqlNode {
public WhereSqlNode(SqlNode contents) {
super(contents, "WHERE", null, "AND", "OR");
}
// 原理:去掉首个AND/OR,并添加WHERE关键字
}
// SetSqlNode自动处理SET逗号
public class SetSqlNode extends TrimSqlNode {
public SetSqlNode(SqlNode contents) {
super(contents, "SET", ",", null, null);
}
// 原理:去掉末尾逗号,并添加SET关键字
}
}
4.3 自定义SQL片段:include
<mapper namespace="com.example.mapper.UserMapper">
<!-- 定义可复用的SQL片段 -->
<sql id="userColumns">
id, name, email, status, create_time
</sql>
<sql id="whereClause">
<where>
<if test="status != null">
status = #{status}
</if>
<if test="keyword != null">
AND (name LIKE #{keyword} OR email LIKE #{keyword})
</if>
</where>
</sql>
<!-- 使用SQL片段 -->
<select id="findAll" resultType="User">
SELECT <include refid="userColumns"/> FROM user
<include refid="whereClause"/>
</select>
<!-- 支持属性覆盖 -->
<select id="findByName" resultType="User">
SELECT <include refid="userColumns">
<property name="tableAlias" value="u"/>
</include>
FROM user u
WHERE u.name = #{name}
</select>
</mapper>
五、插件机制:Interceptor
5.1 插件拦截点
┌─────────────────────────────────────────────────────────────────────┐
│ MyBatis四大拦截点 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Executor(方法级别) │
│ - update, query, flushStatements, commit, rollback, getTransaction
│ - 拦截时机:SQL执行前/后 │
│ - 用途:分页插件、慢SQL日志 │
│ │
│ 2. StatementHandler(SQL构建阶段) │
│ - prepare, parameterize, batch, query, update │
│ - 拦截时机:SQL预编译前/后 │
│ - 用途:SQL改写(分页SQL改写) │
│ │
│ 3. ParameterHandler(参数处理阶段) │
│ - getParameterObject, setParameters │
│ - 拦截时机:设置参数前/后 │
│ - 用途:参数加密、参数处理 │
│ │
│ 4. ResultSetHandler(结果处理阶段) │
│ - handleResultSets, handleOutputParameters │
│ - 拦截时机:结果集处理前/后 │
│ - 用途:结果解密、脱敏 │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 分页插件实战
// 1. 定义分页插件
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PaginationInterceptor implements Interceptor {
private static final String BOUND_SQL_KEY = "-boundSql";
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler)
Plugin.unwrap(invocation.getTarget());
// 1. 获取原始SQL
String originalSql = statementHandler.getBoundSql().getSql();
// 2. 从ThreadLocal获取分页参数
Page<?> page = PageHelper.getLocalPage();
if (page == null) {
return invocation.proceed(); // 无分页参数,放行
}
// 3. 获取BoundSql对象
BoundSql boundSql = statementHandler.getBoundSql();
// 4. 拼接分页SQL(MySQL)
String pageSql = originalSql +
" LIMIT " + (page.getPageNum() - 1) * page.getPageSize() +
", " + page.getPageSize();
// 5. 重新设置SQL(使用反射)
Field field = BoundSql.class.getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, pageSql);
// 6. 继续执行
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 只拦截StatementHandler
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 从配置文件读取插件属性
}
}
// 2. 配置插件
@Configuration
public class MyBatisConfig {
@Bean
public Interceptor[] paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
properties.setProperty("reasonable", "true");
paginationInterceptor.setProperties(properties);
return new Interceptor[]{paginationInterceptor};
}
}
// 3. 使用分页
@Service
public class UserService {
public PageInfo<User> findUserPage(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize); // 设置分页参数
List<User> users = userMapper.selectAll();
return new PageInfo<>(users); // 自动计算总数和分页信息
}
}
5.3 通用Mapper插件
// 通用Mapper插件:为所有Mapper自动添加常用CRUD方法
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class AutoCRUDInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 根据SQL类型自动处理
switch (ms.getSqlCommandType()) {
case INSERT:
return handleInsert(ms, parameter);
case UPDATE:
return handleUpdate(ms, parameter);
case DELETE:
return handleDelete(ms, parameter);
case SELECT:
return invocation.proceed();
}
return invocation.proceed();
}
private int handleInsert(MappedStatement ms, Object parameter) {
// 自动生成ID、创建时间、更新时间等
if (parameter instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) parameter;
if (entity.getId() == null) {
entity.setId(SnowflakeIdGenerator.nextId());
}
if (entity.getCreateTime() == null) {
entity.setCreateTime(new Date());
}
}
return 0; // 继续执行原SQL
}
}
六、关联查询:ResultMap高级映射
6.1 一对一关联
<!-- 订单和用户:订单属于一个用户 -->
<resultMap id="OrderWithUserMap" type="Order">
<!-- 主键映射 -->
<id property="id" column="order_id"/>
<!-- 普通字段映射 -->
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
<!-- 一对一关联:使用association -->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
</association>
</resultMap>
<select id="findOrderWithUser" resultMap="OrderWithUserMap">
SELECT o.id as order_id, o.order_no, o.amount,
u.id as user_id, u.name as user_name, u.email as user_email
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{orderId}
</select>
6.2 一对多关联
<!-- 用户和订单:一个用户有多个订单 -->
<resultMap id="UserWithOrdersMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="user_email"/>
<!-- 一对多关联:使用collection -->
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<!-- N+1问题优化:使用JOIN -->
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
SELECT u.id as user_id, u.name as user_name, u.email as user_email,
o.id as order_id, o.order_no, o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
6.3 嵌套查询 vs 嵌套结果
┌─────────────────────────────────────────────────────────────────────┐
│ 关联查询两种方式对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 方式1:嵌套查询(Separate Query) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ SELECT * FROM user WHERE id = 1 │ │
│ │ ↓ 用户查询 │ │
│ │ SELECT * FROM orders WHERE user_id = 1 │ │
│ │ ↓ 订单查询(根据用户ID) │ │
│ │ SELECT * FROM order_items WHERE order_id IN (1, 2, 3) │ │
│ │ ↓ 订单项查询(根据订单ID列表) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ 问题:N+1查询!每个关联都会触发一次额外查询 │
│ 优点:可以复用单表查询 │
│ 适用:关联表数据量大、不经常同时查询的场景 │
│ │
│ 方式2:嵌套结果(Nested Results) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ SELECT u.*, o.* FROM user u LEFT JOIN orders o ON u.id = o.user_id │
│ │ LEFT JOIN order_items i ON o.id = i.order_id│ │
│ └────────────────────────────────────────────────────────────────┘ │
│ 优点:单次查询解决所有数据 │
│ 问题:大数据量时SQL复杂、结果集大 │
│ 适用:关联数据量可控、经常同时查询的场景 │
│ │
│ 推荐:优先使用嵌套结果,通过JOIN一次性获取 │
│ │
└─────────────────────────────────────────────────────────────────────┘
七、常见面试题
Q1:MyBatis为什么能防止SQL注入?
1. 使用PreparedStatement(预编译)
- SQL结构在编译时确定
- 参数以?占位符形式存在
- 参数不会被当作SQL语句执行
2. 参数绑定使用#{}
- #{name} → 使用PreparedStatement的setString()
- 会进行类型转换和转义处理
3. 区别:#{} vs ${}
- #{name}:预编译参数绑定,防SQL注入
- ${name}:直接字符串替换,不防SQL注入
- ${}使用场景:表名、列名动态确定时(需严格校验)
Q2:MyBatis一级缓存和二级缓存的区别?
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存对比表 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬─────────────────┬─────────────────────────────────┐ │
│ │ │ 一级缓存 │ 二级缓存 │ │
│ ├──────────────┼─────────────────┼─────────────────────────────────┤ │
│ │ 作用域 │ SqlSession │ Mapper级别(全局) │ │
│ │ 存储位置 │ BaseExecutor │ CachingExecutor │ │
│ │ 存储介质 │ PerpetualCache │ 可配置(内存/Redis/Ehcache) │ │
│ │ 生命周期 │ SqlSession周期 │ 应用运行期间 │ │
│ │ 默认开启 │ 是 │ 否(需配置<cache>) │ │
│ │ 失效时机 │ commit/rollback│ commit后生效 │ │
│ └──────────────┴─────────────────┴─────────────────────────────────┘ │
│ │
│ 执行顺序:二级缓存 → 一级缓存 → 数据库 │
│ │
└─────────────────────────────────────────────────────────────────────┘
Q3:MyBatis如何获取自增ID?
<!-- 方式1:useGeneratedKeys + keyProperty -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
<!-- 方式2:selectKey子查询 -->
<insert id="insert">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT IFNULL(MAX(id), 0) + 1 FROM user
</selectKey>
INSERT INTO user(id, name, email) VALUES(#{id}, #{name}, #{email})
</insert>
Q4:MyBatis延迟加载的原理?
原理:使用CGLIB或Javassist生成代理对象
┌─────────────────────────────────────────────────────────────────────┐
│ 延迟加载执行流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 查询主对象(不查询关联对象) │
│ SELECT * FROM user WHERE id = 1 │
│ │
│ 2. 返回代理对象(UserMapperProxy) │
│ User user = userMapper.findById(1); │
│ // user对象实际是代理对象 │
│ │
│ 3. 访问关联属性时触发加载 │
│ System.out.println(user.getOrders()); │
│ // 代理对象拦截getOrders(),执行关联查询 │
│ SELECT * FROM orders WHERE user_id = 1 │
│ │
│ 配置: │
│ <settings> │
│ <setting name="lazyLoadingEnabled" value="true"/> │
│ <setting name="aggressiveLazyLoading" value="false"/> │
│ </settings> │
│ │
└─────────────────────────────────────────────────────────────────────┘
八、性能优化实践
8.1 SQL优化建议
<!-- 1. 使用分页查询,避免SELECT * -->
<select id="findUserPage" resultType="User">
SELECT id, name, email, status
FROM user
WHERE status = 1
LIMIT #{offset}, #{pageSize}
</select>
<!-- 2. 批量操作,减少数据库交互 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user(name, email) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.email})
</foreach>
<!-- 一次SQL插入1000条,减少网络开销 -->
</insert>
<!-- 3. 使用JOIN替代子查询(MySQL优化器优化) -->
<!-- 不推荐:子查询 -->
<select id="findUserWithOrders" resultType="User">
SELECT * FROM user
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000)
</select>
<!-- 推荐:JOIN -->
<select id="findUserWithOrders" resultType="User">
SELECT DISTINCT u.*
FROM user u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000
</select>
<!-- 4. 利用覆盖索引,避免回表 -->
<!-- 假设:index(name, email, status) -->
<select id="findByName" resultType="User">
SELECT name, email FROM user WHERE name = #{name}
<!-- 无需回表,直接在索引中获取数据 -->
</select>
8.2 配置优化
# MyBatis配置优化
mybatis:
configuration:
# 开启驼峰命名转换(数据库下划线 → Java驼峰)
map-underscore-to-camel-case: true
# 开启延迟加载
lazy-loading-enabled: true
aggressive-lazy-loading: false
# 开启二级缓存
cache-enabled: true
# 打印SQL日志(开发环境)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局超时时间
default-statement-timeout: 30
# JDBC类型处理(空值处理)
jdbc-type-for-null: 'null'
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.entity
type-handlers-package: com.example.handler
九、总结
┌─────────────────────────────────────────────────────────────────────┐
│ MyBatis知识地图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 核心组件 ──────────────────────────────────────────────────────→ │
│ SqlSession → Executor → StatementHandler │
│ → ParameterHandler → ResultSetHandler │
│ │
│ 缓存机制 ──────────────────────────────────────────────────────→ │
│ 一级缓存(SqlSession) → 二级缓存(Mapper) │
│ │
│ 动态SQL ──────────────────────────────────────────────────────→ │
│ if/choose/when/otherwise → where/set/trim → foreach/bind │
│ │
│ 插件机制 ──────────────────────────────────────────────────────→ │
│ Interceptor → @Signature → Executor/StatementHandler/... │
│ │
│ 关联映射 ──────────────────────────────────────────────────────→ │
│ association(一对一) → collection(一对多) │
│ │
│ 性能优化 ──────────────────────────────────────────────────────→ │
│ 分页查询 → 批量操作 → 覆盖索引 → 延迟加载 │
│ │
└─────────────────────────────────────────────────────────────────────┘
🎯 讨论话题
大家在使用MyBatis时遇到过哪些"奇葩"问题?是怎么排查和解决的?
往期热门文章推荐:
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!我们下期再见! 👋