深入理解MyBatis延迟加载:原理、配置与实战优化

引言

在Java开发中,数据库操作是绕不开的环节。当我们用MyBatis查询一个对象(比如User)时,如果它关联了其他对象(比如List<Order>订单),传统做法是一次性把所有关联数据都查出来。但这样会有什么问题?

  • 主表数据量小,但关联表数据量大(比如一个用户有1000条订单),查询会变慢;
  • 网络传输和内存占用高,甚至可能触发数据库的"大事务"风险。

这时候,MyBatis的延迟加载(Lazy Loading) 就像一把"优化钥匙"------它能让关联数据"按需加载",只在需要的时候才去查数据库。今天我们就来彻底搞懂它!

一、延迟加载是什么?解决什么问题?

延迟加载(Lazy Loading),直译就是"懒加载"。核心思想是:主对象查询时不立即加载关联对象,而是在实际使用关联数据时再触发查询。

举个栗子🌰:

我们要查用户User的信息,但他关联了100条订单Order。如果不用延迟加载,SQL会是:

sql 复制代码
-- 一次性加载用户+所有订单(1次查询)
SELECT * FROM user WHERE id=1; 
SELECT * FROM order WHERE user_id=1; -- 100条记录

而用了延迟加载,SQL会变成:

sql 复制代码
-- 第一步:只查用户(1次查询)
SELECT * FROM user WHERE id=1; 

-- 第二步:当代码中调用user.getOrders()时,再查订单(1次查询)
SELECT * FROM order WHERE user_id=1;

效果:减少数据库压力,提升响应速度!

二、延迟加载的核心原理:动态代理

MyBatis是如何实现"按需加载"的?答案是动态代理。

当你查询主对象(如User)时,MyBatis不会直接返回真实的User对象,而是生成一个代理对象 (比如UserProxy)。这个代理对象会"包裹"真实的User,并监听你对它的操作:

  • 如果你只访问主对象的属性(如user.getId()),代理对象直接返回真实值,不会触发关联查询;
  • 如果你访问关联对象(如user.getOrders()),代理对象会立刻触发一条关联查询SQL,把数据加载进来,再返回给你。

关键点:延迟加载的触发条件是"访问关联对象的属性",且必须保证数据库连接未关闭(否则无法执行后续查询)。

三、手把手教你配置延迟加载

MyBatis支持全局配置 和局部配置,灵活适配不同场景。

1. 全局配置(推荐新手)

在mybatis-config.xml中开启全局延迟加载,适合所有关联关系都希望延迟加载的场景:

xml 复制代码
<configuration>
  <settings>
    <!-- 开启延迟加载(默认false) -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 激进延迟加载(默认false):是否所有属性访问都触发关联查询(不推荐!) -->
    <setting name="aggressiveLazyLoading" value="false"/>
  </settings>
</configuration>

2. 局部配置(精准控制)

如果只想让某个关联关系延迟加载,可以在映射文件(XML)或注解中单独配置。

场景1:一对多(Collection标签)

比如User和Order的一对多关系,用<collection>标签配置延迟加载:

xml 复制代码
<!-- UserMapper.xml -->
<select id="getUserById" resultMap="userResultMap">
  SELECT * FROM user WHERE id = #{id}
</select>

<resultMap id="userResultMap" type="User">
  <id column="id" property="id"/>
  <result column="username" property="username"/>
  <!-- 一对多延迟加载:指定关联查询的SQL和方法 -->
  <collection 
    property="orders"       <!-- User中的关联属性名 -->
    column="id"             <!-- 传递给关联SQL的参数(外键) -->
    select="com.example.mapper.OrderMapper.getOrdersByUserId"  <!-- 关联查询的SQL ID -->
    fetchType="lazy"/>      <!-- 显式声明延迟加载(可选,默认由全局配置决定) -->
</resultMap>
场景2:多对一(Association标签)

比如Order和User的多对一关系,用<association>标签配置:

xml 复制代码
<!-- OrderMapper.xml -->
<select id="getOrderById" resultMap="orderResultMap">
  SELECT * FROM order WHERE id = #{id}
</select>

<resultMap id="orderResultMap" type="Order">
  <id column="id" property="id"/>
  <result column="amount" property="amount"/>
  <!-- 多对一延迟加载 -->
  <association 
    property="user"         <!-- Order中的关联属性名 -->
    column="user_id"        <!-- 传递给关联SQL的参数(外键) -->
    select="com.example.mapper.UserMapper.getUserById"  <!-- 关联查询的SQL ID -->
    fetchType="lazy"/>      <!-- 延迟加载 -->
</resultMap>
场景3:注解配置(MyBatis 3.3+)

如果用注解开发,可以用@One(多对一)和@Many(一对多)标签:

java 复制代码
public interface OrderMapper {
  @Select("SELECT * FROM order WHERE id = #{id}")
  @Results({
    @Result(property = "id", column = "id"),
    @Result(property = "user", 
            column = "user_id",
            one = @One(select = "getUserById", fetchType = FetchType.LAZY))  // 延迟加载
  })
  Order getOrderById(Long id);
}

// UserMapper中的关联查询方法
public interface UserMapper {
  @Select("SELECT * FROM user WHERE id = #{id}")
  User getUserById(Long id);
}

四、实战避坑:延迟加载的常见问题与优化

问题1:N+1查询问题

假设你要查10个用户,每个用户的订单都要单独查一次,总查询次数是1(主查询)+10(关联查询)=11次,这就是经典的"N+1问题"。

解决方案:批量加载(Batch Loading)

MyBatis支持批量查询关联数据,把多次查询合并成一次。

方式1:XML配置(需CGLIB代理)

在mybatis-config.xml中设置proxyFactory为CGLIB,并配合lazyLoader:

xml 复制代码
<settings>
  <setting name="proxyFactory" value="CGLIB"/> <!-- 启用CGLIB代理 -->
</settings>
方式2:注解配置(@BatchSize)

在查询方法上添加@BatchSize,指定每批加载的数量:

java 复制代码
public interface UserMapper {
  @Select("SELECT * FROM user WHERE id = #{id}")
  @Results({
    @Result(property = "orders", 
            column = "id",
            many = @Many(select = "getOrdersByUserId", fetchType = FetchType.LAZY))
  })
  @BatchSize(size = 5)  <!-- 每批加载5个用户的订单 -->
  User getUserById(Long id);
}

这样,当连续查询5个用户时,会合并成1次SQL:SELECT * FROM order WHERE user_id IN (1,2,3,4,5)。

问题2:事务失效导致延迟加载失败

延迟加载的关联查询需要使用同一个数据库连接 。如果在主查询后关闭了连接(比如退出@Transactional注解的方法),再访问关联数据就会报错Invalid statement or result set closed。

解决方案 :

确保延迟加载的操作在事务范围内 。Spring项目中,用@Transactional注解包裹业务逻辑即可:

java 复制代码
@Service
public class UserService {
  @Autowired
  private UserMapper userMapper;

  @Transactional  // 保证事务内连接不关闭
  public User getUserWithOrders(Long userId) {
    return userMapper.getUserById(userId); // 访问user.getOrders()时会触发延迟加载
  }
}

问题3:循环引用导致栈溢出

如果主对象和关联对象互相引用(比如User有List<Order>,Order又有User),延迟加载可能导致无限递归查询,甚至栈溢出。

解决方案:

  • 在序列化时忽略循环字段(如用@JsonIgnore标记Order中的user属性);
  • 或在查询时关闭其中一个方向的延迟加载(比如Order的user改为立即加载)。

五、总结:延迟加载的最佳实践

  1. 按需使用:高频访问的小数据量关联对象(如用户的姓名、手机号)可以立即加载;低频访问的大数据量关联对象(如用户的订单列表)用延迟加载。
  2. 避免N+1 :用@BatchSize或CGLIB批量加载优化,减少查询次数。
  3. 事务兜底 :所有延迟加载操作必须在事务中执行(Spring的@Transactional是神器)。
  4. 监控SQL :通过MyBatis日志(logImpl=STDOUT_LOGGING)或APM工具(如SkyWalking)监控查询次数,及时发现性能瓶颈。

最后提醒:延迟加载不是"银弹",滥用可能导致复杂度上升。结合业务场景选择合适的加载策略(立即加载/延迟加载),才能让系统性能最大化!

如果觉得本文对你有帮助,欢迎点赞收藏,评论区一起交流~ 😊

相关推荐
vx_Biye_Design6 分钟前
springboot高校选课系统82776-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
Sweet锦1 小时前
不调 Python,不装向量库:我用纯 Java 写了一套以图搜图引擎
java·人工智能·开源·图搜索
如意猴2 小时前
【C++】007 C/C++ 内存管理机制、malloc与new的区别及模板初阶
java·c++·算法
可乐鸡翅yeah_2 小时前
HLS 分片过期清理,直播旧 TS 分片磁盘爆满问题处理
java·后端·spring·m3u8·m3u8在线·音视频在线播放
专业程序开发源3 小时前
SSM校园拍摄交流服务平台36936-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·php·课程设计
用户094248568033 小时前
第23章:OpenJDK逃逸分析、标量替换与锁优化
java·jvm
wuminyu3 小时前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
樱花落木兰3 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
HAHAXX83 小时前
电商RPA批量上架通用方案:一套流程如何同时跑通拼多多、抖店、淘宝和跨境平台
java·运维·rpa
\光辉岁月/3 小时前
3.java运算符
java·开发语言