Skip to content

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 类型sqlCommandTypestatementType典型用途
普通查询SELECTPREPARED默认,预编译防注入
动态表名SELECTSTATEMENT无法预编译的表名/列名
存储过程SELECT/INSERTCALLABLE调用数据库存储过程

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 作为成员变量(线程不安全)
@Service
public class UserService {
private SqlSession sqlSession; // 危险!多线程共享
}
// ✅ 正确:使用 Mapper 接口(Spring 自动代理)
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 线程安全
public User getUser(Long id) {
return userMapper.selectById(id); // 每次调用创建新的 SqlSession
}
}
// ✅ 正确:使用 SqlSessionTemplate(Spring 提供的线程安全实现)
@Service
public 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 次执行 → 耗时 ~8s
ReuseExecutor:10000 次预编译 + 10000 次执行 → 耗时 ~7s(Statement 复用收益小)
BatchExecutor:1 次预编译 + 10000 次缓存 + 1 次批量提交 → 耗时 ~0.5s

源码层面的区别

// SimpleExecutor:每次都创建新 Statement
public class SimpleExecutor extends BaseExecutor {
@Override
public Statement prepareStatement(Connection conn, String sql) {
return conn.prepareStatement(sql); // 每次新建
}
}
// ReuseExecutor:缓存 Statement
public 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 中配置
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}

第四种:CachingExecutor(装饰器模式)

// 二级缓存的实现:用装饰器包装原 Executor
public 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

Q5:MyBatis 的四大核心对象是什么?各自负责什么?必考

Section titled “Q5:MyBatis 的四大核心对象是什么?各自负责什么?”

这是理解 MyBatis 执行流程和插件机制的基础。

四大核心对象

对象职责关键方法创建时机
ExecutorSQL 执行调度器update(), query(), commit(), rollback()SqlSession 创建时
StatementHandlerStatement 管理器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 创建 Statement
StatementHandler 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. 执行 SQL
List<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);

本质一句话#{} 使用预编译参数,安全高效;${} 是字符串替换,有注入风险,仅用于表名、列名等无法预编译的结构参数,且必须在应用层做白名单校验。


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 字段被标记为「待加载」,初始值为 null
3. 当调用 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>

┌────────────────────────────────────────────────────────────────────┐
│ 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 时写入二级缓存 │
└────────────────────────────────────────────────────────────────────┘


// ❌ 低性能:SimpleExecutor,循环插入
@Transactional
public void batchInsertUsers(List<User> users) {
for (User user : users) {
userMapper.insert(user); // 每次 SQL 都预编译 + 执行
}
}
// 性能:10000 条数据,耗时 ~8s
// ✅ 高性能:BatchExecutor,批量提交
@Transactional
public 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 倍)
// ❌ 低性能:延迟加载导致 N+1 查询
List<Order> orders = orderMapper.selectAll(); // 1 次 SQL
for (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 次 SQL
for (Order order : orders) {
System.out.println(order.getOrderNo() + ": " + order.getItems().size());
}
// 性能:1 次 SQL,耗时 ~50ms(提升 40 倍)

性能数据对比

场景SimpleExecutorBatchExecutor提升倍数
批量插入 10000 条8s0.5s16x
批量插入 100000 条80s5s16x
场景延迟加载JOIN 查询提升倍数
查询 100 个 Order + Items2s (101 次 SQL)50ms (1 次 SQL)40x
查询 1000 个 Order + Items20s (1001 次 SQL)500ms (1 次 SQL)40x

  1. Mapper 代理:JDK 动态代理,StatementId = 接口全限定名 + 方法名
  2. SqlSession:线程不安全,每次请求创建新的,Spring 通过 SqlSessionTemplate 代理
  3. Executor 三种类型:Simple(默认)、Reuse(Statement 复用)、Batch(批量提交)
  4. 四大对象:Executor(调度)、StatementHandler(执行)、ParameterHandler(参数)、ResultSetHandler(结果)
  5. #{} vs ${}:预编译参数 vs 字符串替换,后者有 SQL 注入风险
  6. 延迟加载:动态代理实现,但有 N+1 查询问题
  7. 一级缓存:SqlSession 级别,默认开启
  8. 二级缓存:namespace 级别,需手动开启,有脏读风险