懒加载:嵌套查询 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,以官方文档为准)。

四个经典坑

  1. 会话关闭后加载报错:Spring 中查询结束 SqlSession 即关闭,若不在事务里,Service 返回后再访问懒属性会抛异常。对策:业务内完成访问,或方法加 @Transactional 让会话撑到返回前。
  2. N+1 查询:查 100 个用户再逐个 getOrders 就是 101 条 SQL。懒加载只适合少量主记录,列表页请用 join 或"先查父再批量查子"。
  3. equals/hashCode/toString 触发加载:这三个方法默认在 lazyLoadTriggerMethods 名单中,一调用就把对象查全;集合判等、日志打印都可能"被加载"。
  4. 序列化/JSON 输出踩雷:把代理对象直接返回前端(Jackson 序列化)会触发加载,或代理无法序列化而报错;先转 DTO 再返回最稳。

建议

场景推荐
主记录少、子表大懒加载可接受
列表页/分页join 或两次查询,别用懒加载
返回前端 JSON先转 DTO,别裸传代理

懒加载的本质是用"查询延迟"换"首查变快",代价是会话、N+1 与序列化三类坑。先想清访问路径:确实需要就加 @Transactional 撑住会话并转 DTO,不需要就 join 一把查完。

笔记加载中…