懒加载:嵌套查询 lazyLoadingEnabled 与坑
查用户时不想马上查出他的订单,等真正用到再查——这叫懒加载(延迟加载)。MyBatis 通过"嵌套查询 + 全局开关"实现按需加载,用起来方便,但坑集中在会话、性能与序列化上。本节讲原理与四个典型坑。
开启方式
先在配置里打开全局开关:
<settings>
<!-- 开启延迟加载,默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- aggressiveLazyLoading 默认 false(3.4.1 及更早默认 true) -->
</settings>
再在 resultMap 里把子对象声明为"嵌套 select":
<resultMap id="UserMap" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="orders" column="id"
select="com.demo.mapper.OrderMapper.selectByUserId"/>
</resultMap>
效果:查用户时不查订单;调用 user.getOrders() 才触发 selectByUserId。单条映射可用 fetchType="lazy" 局部开启,优先级高于全局设置。
工作原理
MyBatis 返回的是代理对象:属性被访问时由代理拦截,若 SqlSession 还活着就发第二条 SQL 填充。所以懒加载成立的前提是:会话存活 + 能按需发 SQL。默认用 Javassist 生成代理(可配 proxyFactory,以官方文档为准)。
四个经典坑
- 会话关闭后加载报错:Spring 中查询结束 SqlSession 即关闭,若不在事务里,Service 返回后再访问懒属性会抛异常。对策:业务内完成访问,或方法加 @Transactional 让会话撑到返回前。
- N+1 查询:查 100 个用户再逐个 getOrders 就是 101 条 SQL。懒加载只适合少量主记录,列表页请用 join 或"先查父再批量查子"。
- equals/hashCode/toString 触发加载:这三个方法默认在
lazyLoadTriggerMethods名单中,一调用就把对象查全;集合判等、日志打印都可能"被加载"。 - 序列化/JSON 输出踩雷:把代理对象直接返回前端(Jackson 序列化)会触发加载,或代理无法序列化而报错;先转 DTO 再返回最稳。
建议
| 场景 | 推荐 |
|---|---|
| 主记录少、子表大 | 懒加载可接受 |
| 列表页/分页 | join 或两次查询,别用懒加载 |
| 返回前端 JSON | 先转 DTO,别裸传代理 |
懒加载的本质是用"查询延迟"换"首查变快",代价是会话、N+1 与序列化三类坑。先想清访问路径:确实需要就加 @Transactional 撑住会话并转 DTO,不需要就 join 一把查完。