MyBatis SQL 执行流程深度解析
面试官:你用过 MyBatis 吗?能说说调用 Mapper 接口方法后,MyBatis 内部是怎么执行的吗?
你:用过。MyBatis 会通过 JDK 动态代理为 Mapper 接口生成代理对象,调用方法时会根据接口名+方法名找到对应的 MappedStatement,然后通过 SqlSession → Executor → StatementHandler → JDBC 的完整链路执行 SQL。
面试官:能详细说说 Executor 有几种类型?它们的区别是什么?
这道追问把很多人问住了——能说出三种 Executor 的区别和应用场景,并理解插件拦截原理的候选人,才算真正掌握了 MyBatis 的核心机制。
链式追问一:从 Mapper 接口到 SQL
Section titled “链式追问一:从 Mapper 接口到 SQL”Q1:调用 userMapper.selectById(1) 时,MyBatis 是怎么知道要执行哪条 SQL 的?必考
Section titled “Q1:调用 userMapper.selectById(1) 时,MyBatis 是怎么知道要执行哪条 SQL 的?”核心机制:JDK 动态代理
Mapper 接口是纯接口,没有实现类。MyBatis 在启动时,通过 MapperProxyFactory 为每个 Mapper 接口生成动态代理对象。
// MapperProxy 核心逻辑(简化版)public class MapperProxy<T> implements InvocationHandler { private final SqlSession sqlSession; private final Class<T> mapperInterface;
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 根据接口全限定名 + 方法名,拼接出 StatementId // 例如:com.example.UserMapper.selectById String statementId = mapperInterface.getName() + "." + method.getName();
// 2. 从 Configuration 中获取 MappedStatement MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(statementId);
// 3. 委托给 SqlSession 执行 return sqlSession.selectOne(statementId, args); }}关键流程:
1. MyBatis 启动时: - 解析所有 XML 和注解中的 SQL - 为每条 SQL 创建 MappedStatement 对象 - 注册到 Configuration 的 mappedStatements Map 中
2. 调用 Mapper 方法时: - JDK 动态代理拦截方法调用 - 拼接 StatementId = 接口全限定名 + "." + 方法名 - 从 Configuration 中取出对应的 MappedStatement - 执行 SQL本质一句话:Mapper 接口没有实现类,通过 JDK 动态代理在运行时拦截方法调用,用”接口全限定名+方法名”作为 key 找到对应的 SQL 配置。
Q2:MappedStatement 里存了什么?高频
Section titled “Q2:MappedStatement 里存了什么?”MappedStatement 是 MyBatis 对一条 SQL 语句的完整描述,包含执行 SQL 所需的一切信息。
核心字段:
MappedStatement├── id → StatementId(唯一标识)├── sqlSource → SqlSource(包含 SQL 文本和动态 SQL 逻辑)├── resultMaps → 结果映射配置(ResultSet → Java 对象)├── statementType → Statement 类型(PREPARED / STATEMENT / CALLABLE)├── sqlCommandType → SQL 类型(SELECT / INSERT / UPDATE / DELETE)├── fetchSize → JDBC fetchSize(每次抓取行数)├── timeout → 超时时间(秒)├── keyGenerator → 主键生成策略├── keyProperties → 主键属性名├── cache → 关联的二级缓存├── useCache → 是否使用二级缓存├── flushCache → 执行前是否清空缓存└── parameterMap → 参数映射(已废弃,用 @Param 替代)对比不同 SQL 的 MappedStatement:
| SQL 类型 | sqlCommandType | statementType | 典型用途 |
|---|---|---|---|
| 普通查询 | SELECT | PREPARED | 默认,预编译防注入 |
| 动态表名 | SELECT | STATEMENT | 无法预编译的表名/列名 |
| 存储过程 | SELECT/INSERT | CALLABLE | 调用数据库存储过程 |
链式追问二:SqlSession 与 Executor
Section titled “链式追问二:SqlSession 与 Executor”Q3:SqlSession 是干什么的?为什么不建议直接使用?必考
Section titled “Q3:SqlSession 是干什么的?为什么不建议直接使用?”SqlSession 的定位:MyBatis 的核心操作接口,提供增删改查方法。
public interface SqlSession extends Closeable { <T> T selectOne(String statement, Object parameter); <E> List<E> selectList(String statement, Object parameter); int insert(String statement, Object parameter); int update(String statement, Object parameter); int delete(String statement, Object parameter); void commit(); void rollback(); // ...}为什么不建议直接使用:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 线程不安全 | 内部封装了 Connection 和 Executor | 每次请求创建新的 SqlSession |
| 资源泄漏 | 忘记关闭会导致连接泄漏 | 使用 try-with-resources |
| 事务管理复杂 | 手动 commit/rollback 容易出错 | 集成 Spring 事务管理 |
在 Spring 中的正确使用:
// ❌ 错误:SqlSession 作为成员变量(线程不安全)@Servicepublic class UserService { private SqlSession sqlSession; // 危险!多线程共享}
// ✅ 正确:使用 Mapper 接口(Spring 自动代理)@Servicepublic class UserService { @Autowired private UserMapper userMapper; // 线程安全
public User getUser(Long id) { return userMapper.selectById(id); // 每次调用创建新的 SqlSession }}
// ✅ 正确:使用 SqlSessionTemplate(Spring 提供的线程安全实现)@Servicepublic class UserService { @Autowired private SqlSessionTemplate sqlSessionTemplate;
public User getUser(Long id) { return sqlSessionTemplate.selectOne("com.example.UserMapper.selectById", id); }}Spring 集成原理:
Mapper 接口调用 └── MapperProxy.invoke() └── SqlSessionTemplate.selectOne() └── SqlSessionInterceptor.invoke() ← 动态代理 └── 从 SqlSessionFactory 获取新的 SqlSession └── 执行 SQL └── 自动关闭 SqlSession(finally 块)Q4:Executor 有几种类型?区别是什么?必考
Section titled “Q4:Executor 有几种类型?区别是什么?”这是 MyBatis 面试的高频考点,三种 Executor 的区别必须掌握。
三种 Executor 对比:
| Executor 类型 | 特点 | Statement 管理 | 适用场景 | 性能 |
|---|---|---|---|---|
| SimpleExecutor | 每次执行创建新 Statement | 不复用 | 默认,适合大多数场景 | 中 |
| ReuseExecutor | 缓存 Statement,相同 SQL 复用 | 复用 | 相同 SQL 频繁执行 | 高 |
| BatchExecutor | 批量提交,不立即执行 | 批量 | 批量插入/更新 | 最高 |
性能对比(批量插入 10000 条数据):
SimpleExecutor:10000 次预编译 + 10000 次执行 → 耗时 ~8sReuseExecutor:10000 次预编译 + 10000 次执行 → 耗时 ~7s(Statement 复用收益小)BatchExecutor:1 次预编译 + 10000 次缓存 + 1 次批量提交 → 耗时 ~0.5s源码层面的区别:
// SimpleExecutor:每次都创建新 Statementpublic class SimpleExecutor extends BaseExecutor { @Override public Statement prepareStatement(Connection conn, String sql) { return conn.prepareStatement(sql); // 每次新建 }}
// ReuseExecutor:缓存 Statementpublic class ReuseExecutor extends BaseExecutor { private final Map<String, Statement> statementMap = new HashMap<>();
@Override public Statement prepareStatement(Connection conn, String sql) { Statement statement = statementMap.get(sql); if (statement == null) { statement = conn.prepareStatement(sql); statementMap.put(sql, statement); // 缓存起来 } return statement; }}
// BatchExecutor:批量提交public class BatchExecutor extends BaseExecutor { private final List<Statement> statementList = new ArrayList<>();
@Override public void doUpdate(MappedStatement ms, Object parameter) { PreparedStatement ps = conn.prepareStatement(sql); ps.setParameter(...); ps.addBatch(); // 加入批处理队列,不立即执行 statementList.add(ps); }
@Override public List<BatchResult> doFlushStatements() { for (Statement ps : statementList) { ps.executeBatch(); // 批量提交 } }}如何选择 Executor 类型:
// 方式1:创建 SqlSession 时指定SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 方式2:全局配置(mybatis-config.xml)<settings> <setting name="defaultExecutorType" value="BATCH"/></settings>
// 方式3:Spring 中配置@Beanpublic SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);}第四种:CachingExecutor(装饰器模式)
// 二级缓存的实现:用装饰器包装原 Executorpublic class CachingExecutor implements Executor { private final Executor delegate; // 被装饰的 Executor
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds) { // 1. 先查二级缓存 Cache cache = ms.getCache(); if (cache != null) { List<E> cachedResult = cache.getObject(cacheKey); if (cachedResult != null) { return cachedResult; // 缓存命中 } }
// 2. 缓存未命中,委托给原 Executor 执行 List<E> result = delegate.query(ms, parameter, rowBounds);
// 3. 写入二级缓存 if (cache != null) { cache.putObject(cacheKey, result); }
return result; }}Executor 装饰链:
CachingExecutor(二级缓存装饰器) └── SimpleExecutor / ReuseExecutor / BatchExecutor └── BaseExecutor(一级缓存基类) └── JDBC Connection链式追问三:四大核心对象
Section titled “链式追问三:四大核心对象”Q5:MyBatis 的四大核心对象是什么?各自负责什么?必考
Section titled “Q5:MyBatis 的四大核心对象是什么?各自负责什么?”这是理解 MyBatis 执行流程和插件机制的基础。
四大核心对象:
| 对象 | 职责 | 关键方法 | 创建时机 |
|---|---|---|---|
| Executor | SQL 执行调度器 | update(), query(), commit(), rollback() | SqlSession 创建时 |
| StatementHandler | Statement 管理器 | prepare(), parameterize(), query() | Executor 执行时 |
| ParameterHandler | 参数处理器 | getParameterObject(), setParameters() | StatementHandler 创建时 |
| ResultSetHandler | 结果集处理器 | handleResultSets(), handleOutputParameters() | StatementHandler 创建时 |
四大对象的协作关系:
┌─────────────────────────────────────────────────────────────┐│ SqlSession ││ (会话层:对外提供 CRUD 接口) │└──────────────────────┬──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Executor ││ (执行器:SQL 调度、缓存管理、事务管理) ││ - SimpleExecutor / ReuseExecutor / BatchExecutor ││ - CachingExecutor(二级缓存装饰器) │└──────────────────────┬──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ StatementHandler ││ (语句处理器:创建 Statement、执行 SQL) ││ - PreparedStatementHandler(默认,预编译) ││ - CallableStatementHandler(存储过程) │└──────────┬───────────────────────────────────┬──────────────┘ │ │ ▼ ▼┌──────────────────────┐ ┌──────────────────────────┐│ ParameterHandler │ │ ResultSetHandler ││ (参数处理器) │ │ (结果集处理器) ││ - #{} 参数绑定 │ │ - ResultSet → JavaBean ││ - 类型转换 │ │ - ResultMap 映射 │└──────────────────────┘ └──────────────────────────┘完整执行链路:
// 1. SqlSession 接收请求sqlSession.selectList("com.example.UserMapper.selectAll");
// 2. Executor 执行调度executor.query(ms, parameter, rowBounds, resultHandler);
// 3. StatementHandler 创建 StatementStatementHandler handler = configuration.newStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);Connection conn = getConnection();PreparedStatement ps = handler.prepare(conn, transaction.getTimeout());
// 4. ParameterHandler 填充参数handler.parameterize(ps); // 内部调用 ParameterHandler.setParameters(ps)
// 5. 执行 SQLList<E> result = handler.query(ps, resultHandler); // 内部调用 ps.executeQuery()
// 6. ResultSetHandler 映射结果List<E> result = resultSetHandler.handleResultSets(ps); // ResultSet → List<User>Q6:#{} 和 ${} 的区别?为什么 ${} 有 SQL 注入风险?必考
Section titled “Q6:#{} 和 ${} 的区别?为什么 ${} 有 SQL 注入风险?”这是 MyBatis 面试必考题,必须深刻理解底层原理。
核心区别对比:
| 特性 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译参数(? 占位符) | 字符串直接替换 |
| SQL 注入 | ✅ 安全,参数化查询 | ❌ 危险,直接拼接 |
| 底层实现 | PreparedStatement.setXxx() | 字符串替换(TextSqlNode) |
| 适用场景 | 值参数(WHERE 条件、INSERT 值) | 结构参数(表名、列名、ORDER BY) |
| 性能 | 高(SQL 缓存,只编译一次) | 低(每次都需要重新编译) |
执行流程对比:
<!-- #{}} 方式 --><select id="selectById" resultType="User"> SELECT * FROM user WHERE id = #{id}</select>
<!-- 生成的 SQL(预编译) -->SELECT * FROM user WHERE id = ?<!-- 参数通过 PreparedStatement.setInt(1, id) 传入 -->
<!-- ${}} 方式 --><select id="selectByTable" resultType="User"> SELECT * FROM ${tableName} WHERE id = #{id}</select>
<!-- 生成的 SQL(字符串替换) -->SELECT * FROM user_dynamic_202401 WHERE id = ?<!-- ${tableName} 被直接替换为 "user_dynamic_202401" -->SQL 注入演示:
// ❌ 危险示例:使用 ${} 拼接 WHERE 条件<select id="selectByName" resultType="User"> SELECT * FROM user WHERE name = '${name}'</select>
// 攻击者输入:name = "admin' OR '1'='1"// 实际执行的 SQL:SELECT * FROM user WHERE name = 'admin' OR '1'='1'// 结果:返回所有用户数据(SQL 注入成功)
// ✅ 安全示例:使用 #{}<select id="selectByName" resultType="User"> SELECT * FROM user WHERE name = #{name}</select>
// 攻击者输入:name = "admin' OR '1'='1"// 实际执行的 SQL:SELECT * FROM user WHERE name = ?// 参数传入:"admin' OR '1'='1"(作为普通字符串)// 结果:查不到数据(SQL 注入失败)底层源码分析:
// #{} 由 ParameterHandler 处理public class DefaultParameterHandler implements ParameterHandler { @Override public void setParameters(PreparedStatement ps) { // 遍历参数映射 for (ParameterMapping mapping : parameterMappings) { Object value = getParameterValue(mapping); // 使用 PreparedStatement.setXxx() 设置参数 TypeHandler typeHandler = mapping.getTypeHandler(); typeHandler.setParameter(ps, i + 1, value); } }}
// ${} 由 TextSqlNode 处理(字符串替换)public class TextSqlNode implements SqlNode { @Override public boolean apply(DynamicContext context) { // 使用 OGNL 表达式直接替换 ${} 为变量值 GenericTokenParser parser = new GenericTokenParser("${", "}", content -> { Object value = OgnlCache.getValue(content, context.getBindings()); return value == null ? "" : value.toString(); // 直接字符串替换! }); context.appendSql(parser.parse(text)); return true; }}正确使用 ${} 的场景:
<!-- 动态表名 --><select id="selectByMonth" resultType="Order"> SELECT * FROM order_${yearMonth} WHERE user_id = #{userId}</select>
<!-- 动态列名(ORDER BY) --><select id="selectUsers" resultType="User"> SELECT * FROM user ORDER BY ${orderColumn} ${orderDirection}</select>
<!-- 动态数据库(跨库查询) --><select id="selectFromDB" resultType="User"> SELECT * FROM ${dbName}.user WHERE id = #{id}</select>安全防护措施(使用 ${} 时):
// 1. 白名单校验(应用层)public List<User> selectByTable(String tableName) { if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException("非法表名"); } return userMapper.selectByTable(tableName);}
// 2. 正则校验(应用层)if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { throw new IllegalArgumentException("非法表名");}
// 3. 使用 @SelectProvider 动态生成 SQL(更安全)@SelectProvider(type = UserSqlProvider.class, method = "selectByTable")List<User> selectByTable(@Param("tableName") String tableName);本质一句话:#{} 使用预编译参数,安全高效;${} 是字符串替换,有注入风险,仅用于表名、列名等无法预编译的结构参数,且必须在应用层做白名单校验。
链式追问四:延迟加载
Section titled “链式追问四:延迟加载”Q7:MyBatis 的延迟加载是怎么实现的?有什么问题?高频
Section titled “Q7:MyBatis 的延迟加载是怎么实现的?有什么问题?”应用场景:查询 Order 时,不立即查询关联的 OrderItem,等到真正使用时才触发查询。
配置方式:
<!-- mybatis-config.xml 开启延迟加载 --><settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> <!-- aggressiveLazyLoading=false:按需加载(只加载用到的属性) --> <!-- aggressiveLazyLoading=true:任意方法调用都加载所有延迟属性 --></settings>
<!-- Mapper XML --><resultMap id="orderResultMap" type="Order"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <!-- 延迟加载关联的 items --> <collection property="items" column="id" select="selectOrderItems" fetchType="lazy"/></resultMap>
<select id="selectOrder" resultMap="orderResultMap"> SELECT * FROM `order` WHERE id = #{id}</select>
<select id="selectOrderItems" resultType="OrderItem"> SELECT * FROM order_item WHERE order_id = #{orderId}</select>实现原理:动态代理
1. 查询 Order,返回 Order 的代理对象(CGLIB 或 Javassist 生成)2. 代理对象中 items 字段被标记为「待加载」,初始值为 null3. 当调用 order.getItems() 时4. 代理拦截器检测到 items 未加载5. 触发额外的 SQL 查询 → SELECT * FROM order_item WHERE order_id = ?6. 填充 items 字段,返回结果7. 再次调用 getItems(),直接返回已加载的数据(不再查询)源码分析:
// ResultLoaderMap:记录延迟加载的属性public class ResultLoaderMap { // key: 属性名, value: 延迟加载器 private final Map<String, LoadPair> loaderMap = new HashMap<>();
public boolean hasLoader(String property) { return loaderMap.containsKey(property); }
public void load(String property) { LoadPair pair = loaderMap.remove(property); if (pair != null) { pair.load(); // 触发 SQL 查询 } }}
// 代理对象拦截器public class ResultObjectProxy implements MethodInterceptor { private ResultLoaderMap loaderMap;
@Override public Object intercept(Object obj, Method method, Object[] args) { String propertyName = getPropertyName(method); // 从 getter 方法提取属性名
if (loaderMap.hasLoader(propertyName)) { loaderMap.load(propertyName); // 触发延迟加载 }
return method.invoke(obj, args); // 调用原方法 }}延迟加载的问题:N+1 查询
场景:查询 100 个 Order,每个 Order 都要查询关联的 OrderItem
第 1 次 SQL:SELECT * FROM `order` LIMIT 100第 2 次 SQL:SELECT * FROM order_item WHERE order_id = 1第 3 次 SQL:SELECT * FROM order_item WHERE order_id = 2...第 101 次 SQL:SELECT * FROM order_item WHERE order_id = 100
结果:1 + 100 = 101 次 SQL(N+1 问题)性能影响:原本 1 次 JOIN 搞定,现在变成 101 次,性能下降几十倍解决方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JOIN 查询 | 一次 SQL,性能最高 | 结果集重复(一对多时) | 数据量小、关联简单 |
| 嵌套查询 + fetchType=“eager” | SQL 简单,避免结果集重复 | N+1 问题,性能差 | 数据量极小 |
| 批量延迟加载 | 按需加载,减少 SQL 次数 | 实现复杂 | 只有用到关联数据才加载 |
最佳实践:
<!-- 方案1:JOIN 查询(推荐) --><select id="selectOrderWithItems" resultMap="orderResultMap"> SELECT o.*, oi.* FROM `order` o LEFT JOIN order_item oi ON o.id = oi.order_id WHERE o.id = #{id}</select>
<!-- 方案2:分批查询(适用于大批量) --><!-- 第一步:查询 Order 列表 --><select id="selectOrders" resultType="Order"> SELECT * FROM `order` LIMIT 100</select>
<!-- 第二步:批量查询 OrderItem(应用层合并) --><select id="selectOrderItemsByOrderIds" resultType="OrderItem"> SELECT * FROM order_item WHERE order_id IN <foreach collection="orderIds" item="id" open="(" separator="," close=")"> #{id} </foreach></select>完整执行流程图
Section titled “完整执行流程图”┌────────────────────────────────────────────────────────────────────┐│ Mapper.method() ││ (Mapper 接口方法调用) │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ MapperProxy.invoke() ││ (JDK 动态代理拦截) ││ - 拼接 StatementId = 接口全限定名 + "." + 方法名 ││ - 从 Configuration 获取 MappedStatement │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ SqlSession.selectList(statementId, parameter) ││ (会话层:对外提供 CRUD 接口) │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ CachingExecutor.query() ││ (二级缓存装饰器) ││ - 检查二级缓存(namespace 级别) ││ - 缓存命中 → 直接返回 ││ - 缓存未命中 → 委托给 BaseExecutor │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ BaseExecutor.query() ││ (一级缓存 + 执行器基类) ││ - 检查一级缓存(SqlSession 级别) ││ - 缓存命中 → 直接返回 ││ - 缓存未命中 → 调用 doQuery() │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ SimpleExecutor.doQuery() ││ (具体执行器实现) ││ - 创建 StatementHandler ││ - 准备 Statement ││ - 执行查询 │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ StatementHandler.prepare() ││ (创建 PreparedStatement) ││ - Connection.prepareStatement(sql) ││ - 设置 fetchSize、timeout 等参数 │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ ParameterHandler.setParameters() ││ (参数绑定) ││ - 遍历 ParameterMapping ││ - 通过 TypeHandler.setParameter(ps, i, value) ││ - 处理 #{} 参数(预编译) │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ PreparedStatement.execute() ││ (JDBC 执行 SQL) ││ - ps.executeQuery() ││ - 返回 ResultSet │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ ResultSetHandler.handleResultSets() ││ (结果集映射) ││ - 遍历 ResultSet ││ - 根据 ResultMap 映射字段 → JavaBean 属性 ││ - 处理嵌套结果映射、延迟加载 ││ - 返回 List<User> │└───────────────────────────┬────────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────────┐│ 返回结果 ││ - 写入一级缓存 ││ - SqlSession close/commit 时写入二级缓存 │└────────────────────────────────────────────────────────────────────┘高频面试题总结
Section titled “高频面试题总结”实战案例:性能优化
Section titled “实战案例:性能优化”案例 1:批量插入优化
Section titled “案例 1:批量插入优化”// ❌ 低性能:SimpleExecutor,循环插入@Transactionalpublic void batchInsertUsers(List<User> users) { for (User user : users) { userMapper.insert(user); // 每次 SQL 都预编译 + 执行 }}// 性能:10000 条数据,耗时 ~8s
// ✅ 高性能:BatchExecutor,批量提交@Transactionalpublic void batchInsertUsers(List<User> users) { SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = batchSession.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); // 只缓存,不立即执行 } batchSession.flushStatements(); // 批量提交 batchSession.commit(); } finally { batchSession.close(); }}// 性能:10000 条数据,耗时 ~0.5s(提升 16 倍)案例 2:N+1 查询优化
Section titled “案例 2:N+1 查询优化”// ❌ 低性能:延迟加载导致 N+1 查询List<Order> orders = orderMapper.selectAll(); // 1 次 SQLfor (Order order : orders) { List<OrderItem> items = order.getItems(); // N 次 SQL System.out.println(order.getOrderNo() + ": " + items.size());}// 性能:1 + N 次 SQL,N=100 时耗时 ~2s
// ✅ 高性能:JOIN 查询List<Order> orders = orderMapper.selectAllWithItems(); // 1 次 SQLfor (Order order : orders) { System.out.println(order.getOrderNo() + ": " + order.getItems().size());}// 性能:1 次 SQL,耗时 ~50ms(提升 40 倍)性能数据对比:
| 场景 | SimpleExecutor | BatchExecutor | 提升倍数 |
|---|---|---|---|
| 批量插入 10000 条 | 8s | 0.5s | 16x |
| 批量插入 100000 条 | 80s | 5s | 16x |
| 场景 | 延迟加载 | JOIN 查询 | 提升倍数 |
|---|---|---|---|
| 查询 100 个 Order + Items | 2s (101 次 SQL) | 50ms (1 次 SQL) | 40x |
| 查询 1000 个 Order + Items | 20s (1001 次 SQL) | 500ms (1 次 SQL) | 40x |
关键知识点速记
Section titled “关键知识点速记”- Mapper 代理:JDK 动态代理,StatementId = 接口全限定名 + 方法名
- SqlSession:线程不安全,每次请求创建新的,Spring 通过 SqlSessionTemplate 代理
- Executor 三种类型:Simple(默认)、Reuse(Statement 复用)、Batch(批量提交)
- 四大对象:Executor(调度)、StatementHandler(执行)、ParameterHandler(参数)、ResultSetHandler(结果)
#{}vs${}:预编译参数 vs 字符串替换,后者有 SQL 注入风险- 延迟加载:动态代理实现,但有 N+1 查询问题
- 一级缓存:SqlSession 级别,默认开启
- 二级缓存:namespace 级别,需手动开启,有脏读风险