深入理解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标签)

比如UserOrder的一对多关系,用<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标签)

比如OrderUser的多对一关系,用<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中设置proxyFactoryCGLIB,并配合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:循环引用导致栈溢出

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

解决方案

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

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

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

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

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

相关推荐
没有bug.的程序员7 分钟前
JAVA面试宝典 - 《MyBatis 进阶:插件开发与二级缓存》
java·面试·mybatis
没有羊的王K2 小时前
SSM框架学习——day1
java·学习
又菜又爱coding2 小时前
安装Keycloak并启动服务(macOS)
java·keycloak
不知道叫什么呀2 小时前
【C】vector和array的区别
java·c语言·开发语言·aigc
wan_da_ren3 小时前
JVM监控及诊断工具-GUI篇
java·开发语言·jvm·后端
cui_hao_nan3 小时前
JAVA并发——什么是Java的原子性、可见性和有序性
java·开发语言
best_virtuoso3 小时前
JAVA JVM垃圾收集
java·开发语言·jvm
lifallen3 小时前
Kafka 时间轮深度解析:如何O(1)处理定时任务
java·数据结构·分布式·后端·算法·kafka
顾林海3 小时前
Android 性能优化:启动优化全解析
android·java·面试·性能优化·zygote
risc1234565 小时前
BKD 树(Block KD-Tree)Lucene
java·数据结构·lucene