题目
MyBatis 多表关联查询中如何解决 N+1 问题并实现高性能延迟加载?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
延迟加载原理,N+1问题优化,动态代理应用,关联查询配置,性能调优
快速回答
解决 N+1 问题的核心方案:
- 启用全局延迟加载配置:
lazyLoadingEnabled=true - 使用
<collection>/<association>的fetchType='lazy' - 结合
aggressiveLazyLoading=false防止侵入式加载 - 通过
@MapperScan确保动态代理生效 - 使用
BatchExecutor批量加载优化性能
1. N+1 问题本质
当主查询返回 N 条记录,每条记录触发 1 次关联子查询时,产生 N+1 次数据库访问:
<!-- 主查询 -->
<select id="selectUsers" resultMap="userMap">
SELECT * FROM users
</select>
<!-- 关联查询(触发 N 次) -->
<select id="selectOrders" resultType="Order">
SELECT * FROM orders WHERE user_id = #{id}
</select>
2. 延迟加载原理
MyBatis 通过 动态代理 实现延迟加载:
- 创建实体类的代理对象(如
User$Proxy) - 首次访问关联属性时触发
MethodInterceptor - 通过
ResultLoader执行子查询 - 利用
ProxyFactory生成 CGLIB/Javassist 代理
3. 完整配置方案
<!-- mybatis-config.xml -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="defaultExecutorType" value="BATCH"/> <!-- 批量执行器 -->
</settings>
<!-- Mapper XML -->
<resultMap id="userMap" type="User">
<collection
property="orders"
column="id"
select="selectOrdersByUserId"
fetchType="lazy" /> <!-- 关键配置 -->
</resultMap>
4. 批量加载优化
使用 @Param 实现批量 ID 查询:
// Mapper接口
@Select("<script> SELECT * FROM orders WHERE user_id IN " +
"<foreach item='id' collection='userIds' open='(' separator=',' close=')'>" +
"#{id}</foreach> </script>")
List<Order> batchLoadOrders(@Param("userIds") List<Long> userIds);
// 实体类增强
public class User {
private List<Order> orders;
// 添加批量加载标识
private boolean ordersLoaded;
}
5. 最佳实践与陷阱
- 会话生命周期:延迟加载需在
SqlSession存活期间完成 - 序列化风险:代理对象序列化时可能触发意外查询
- 性能监控:通过
org.apache.ibatis.logging跟踪查询触发 - 替代方案:复杂场景改用 JOIN 查询 +
ResultMap嵌套映射
6. 扩展:二级缓存陷阱
延迟加载对象缓存时:
User user1 = session.selectOne("selectUser", 1);
user1.getOrders().size(); // 触发查询
session.close();
// 从缓存获取时
User user2 = session.selectOne("selectUser", 1);
user2.getOrders(); // 可能得到未初始化的代理对象!
解决方案:禁用关联对象的二级缓存或实现 Serialization 深度复制