【MyBatis延迟加载大揭秘】性能优化神器!懒加载原理与实战全解析
🚗立即加载:买一辆车,强制附带所有配件(即使你只需要方向盘)🛋️延迟加载:买一辆车,其他配件按需购买(需要时才购买空调/导航)配置三步走fill:#333;color:#333;color:#333;fill:none;启用lazyLoadingEnabled关闭aggressiveLazyLoading按需配置fetchType遵循最佳实践频繁使用的关联 → 立即加载大对象/不常用关联 →
·
你是否遇到过查询用户信息却意外加载了所有订单数据的情况?MyBatis的延迟加载就是解决这个痛点的神器!本文将带你深入理解这个提升性能的黑科技!
一、什么是延迟加载?为什么需要它?🤔
生活化比喻:
- 🚗 立即加载:买一辆车,强制附带所有配件(即使你只需要方向盘)
- 🛋️ 延迟加载:买一辆车,其他配件按需购买(需要时才购买空调/导航)
技术场景:
// 查询用户(包含订单集合)
User user = userDao.findById(1);
// 立即加载:执行上面代码时,用户+所有订单数据都已加载
// 延迟加载:只加载用户数据,访问订单时才加载订单数据
延迟加载的价值:
| 场景 | 立即加载 | 延迟加载 |
|---|---|---|
| 查询用户基本信息 | 加载用户+所有订单(浪费) | 只加载用户(高效) |
| 查看用户详情 | 已加载所有数据(响应快) | 首次访问关联数据稍慢 |
| 系统资源占用 | 内存消耗大,数据库压力大 | 资源按需使用,优化整体性能 |
二、延迟加载核心原理揭秘
1. 整体执行流程
2. 代理对象生成原理
关键点:
- 创建主对象时,关联属性用代理对象占位
- 首次访问代理属性时,触发真实查询
- 查询结果替换代理对象成为真实数据
三、延迟加载配置实战 🛠️
1. 全局启用延迟加载
<!-- mybatis-config.xml -->
<settings>
<!-- 启用延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 按需加载(默认false,2.3.0后默认true) -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 指定触发加载的方法(默认equals,clone,hashCode,toString) -->
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
2. 关联映射配置
<!-- UserMapper.xml -->
<resultMap id="userWithOrders" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 配置订单集合的延迟加载 -->
<collection
property="orders"
ofType="Order"
select="com.example.mapper.OrderMapper.findByUserId"
column="id"
fetchType="lazy"/> <!-- 关键! -->
</resultMap>
<select id="findById" resultMap="userWithOrders">
SELECT * FROM users WHERE id = #{id}
</select>
3. 注解方式配置
public interface UserMapper {
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "orders", column = "id",
many = @Many(
select = "com.example.mapper.OrderMapper.findByUserId",
fetchType = FetchType.LAZY // 延迟加载
))
})
@Select("SELECT * FROM users WHERE id = #{id}")
User findByIdWithOrders(int id);
}
四、深度解析代理实现 🔍
1. 代理对象生成代码(简化版)
public class UserProxy extends User {
private MethodHandler handler;
@Override
public List<Order> getOrders() {
// 首次访问时加载真实数据
if (handler != null) {
handler.invoke(); // 触发SQL查询
handler = null; // 清除处理器
}
return super.getOrders();
}
}
// MyBatis创建代理对象
public User createProxy(User user) {
UserProxy proxy = new UserProxy();
proxy.setId(user.getId());
proxy.setName(user.getName());
proxy.setHandler(new LazyLoader(user.getId()));
return proxy;
}
2. 加载触发时序图
五、延迟加载的四种场景
1. 集合延迟加载(最常见)
User user = userMapper.findById(1);
// 此时只查询用户表
System.out.println(user.getOrders());
// 触发查询:SELECT * FROM orders WHERE user_id=1
2. 关联对象延迟加载
<resultMap id="orderDetail" type="Order">
<association
property="product"
select="com.example.mapper.ProductMapper.findById"
column="product_id"
fetchType="lazy"/>
</resultMap>
3. 嵌套查询延迟
User user = userMapper.findById(1);
// 只加载用户
Order firstOrder = user.getOrders().get(0);
// 触发加载所有订单
Product product = firstOrder.getProduct();
// 触发加载商品信息
4. 深度延迟加载
访问路径:
- 访问用户 → 只查用户
- 访问用户.orders → 查订单
- 访问用户.orders[0].product → 查商品
- 访问user.orders[0].product.supplier → 查供应商
六、性能优化最佳实践 🚀
1. 配置策略对比
| 配置项 | 值 | 效果 | 适用场景 |
|---|---|---|---|
| lazyLoadingEnabled | true | 启用延迟加载 | 大多数关联查询场景 |
| aggressiveLazyLoading | false | 按需加载(访问属性才加载) | 推荐默认设置 |
| true | 积极加载(访问任何属性即加载所有关联) | 不推荐 | |
| lazyLoadTriggerMethods | “” | 自定义触发方法(空表示仅属性访问触发) | 精细控制 |
2. 避免N+1查询问题
问题场景:
List<User> users = userMapper.findAll();
for (User user : users) {
// 每次循环触发一次查询
System.out.println(user.getOrders().size());
}
// 执行1(主查询)+ N(关联查询)次SQL
解决方案:
<!-- 使用联接查询一次性加载 -->
<select id="findAllWithOrders" resultMap="userWithOrders">
SELECT u.*, o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
</select>
3. 混合加载策略
<resultMap id="advancedResultMap" type="User">
<!-- 立即加载基本信息 -->
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 延迟加载订单 -->
<collection property="orders" fetchType="lazy" ... />
<!-- 立即加载常用地址 -->
<association property="primaryAddress" fetchType="eager" ... />
</resultMap>
七、常见问题解决方案
1. 序列化问题
现象:延迟加载对象序列化时报错
原因:代理对象未实现序列化接口
解决:
// 实体类实现Serializable
public class User implements Serializable {
// ...
}
2. 会话关闭问题
现象:访问延迟属性时报Executor was closed
原因:SqlSession已关闭
解决:
// 方案1:使用OpenSessionInView模式(Web应用)
// 方案2:在事务内访问所有需要的数据
try (SqlSession session = factory.openSession()) {
User user = userMapper.findById(1);
// 在session关闭前访问延迟数据
user.getOrders().forEach(System.out::println);
}
3. 性能监控
// 检测延迟加载触发次数
public class LazyListener implements ObjectFactory {
@Override
public void onLazyLoad(String property) {
logger.debug("延迟加载触发: " + property);
// 监控代码...
}
}
// 配置文件中注册
<objectFactory type="com.example.LazyListener"/>
八、延迟加载 vs 立即加载
| 特性 | 延迟加载 | 立即加载 |
|---|---|---|
| 查询时机 | 访问属性时触发 | 主查询时立即加载 |
| SQL数量 | 1+N(按需触发) | 1(或通过join合并) |
| 内存占用 | 初始占用小 | 初始占用大 |
| 响应速度 | 首次访问关联数据稍慢 | 首次响应快 |
| 适用场景 | 关联数据不常用 | 关联数据总是需要 |
| 复杂度 | 需处理会话生命周期 | 实现简单 |
九、总结:延迟加载的正确打开方式
-
配置三步走:
-
遵循最佳实践:
- 频繁使用的关联 → 立即加载
- 大对象/不常用关联 → 延迟加载
- 循环中使用 → 避免N+1问题
-
避坑指南:
- 实体类实现
Serializable - 保持
SqlSession开启直到使用完延迟数据 - 生产环境监控延迟加载触发频率
- 实体类实现
💡 性能黄金法则:按需加载是优化数据库交互的最高原则!
最后的小测试:
当aggressiveLazyLoading=true时,调用user.toString()会发生什么?
A) 只加载用户基本信息
B) 加载所有延迟属性
C) 抛出异常
(答案:B - 因为toString是默认的触发方法之一)
掌握延迟加载,让你的应用性能飞起来!现在就去优化你的代码吧~ 🚀
更多推荐


所有评论(0)