当前位置: 首页 > news >正文

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

引言

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

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

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

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

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

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

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

而用了延迟加载,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中开启全局延迟加载,适合所有关联关系都希望延迟加载的场景:

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

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

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

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

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

<!-- 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>标签配置:

<!-- 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(一对多)标签:

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

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

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

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注解包裹业务逻辑即可:

@Service
public class UserService {@Autowiredprivate 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)监控查询次数,及时发现性能瓶颈。

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

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

http://www.dtcms.com/a/278718.html

相关文章:

  • 浏览器自动化领域的MCP
  • Ubuntu22.04 python环境管理
  • 前端常见十大问题讲解
  • priority_queue的使用和模拟实现以及仿函数
  • 【记忆化搜索 BFS】P9038 [PA 2021] Butelki|普及+
  • 赋能公安行业信息化PPT(46页)
  • 软考 系统架构设计师系列知识点之杂项集萃(111)
  • [C语言语法笔记] 批量处理错误 goto
  • make_ext4fs工具详解
  • Why C# and .NET are still relevant in 2025
  • Windows 上安装 FFmpeg
  • Spring的`@Value`注解使用详细说明
  • Git 使用技巧与原理(一)—— 基础操作
  • SpringMVC3
  • 后端接口通用返回格式与异常处理实现
  • SpringMVC2
  • C++中STL六大组件List的简单介绍
  • 基于GA遗传优化的多边形拟合算法matlab仿真
  • 能源管理系统中的物联网数据采集:深度探索与操作指南
  • AI Linux 运维笔记
  • vmware使用说明
  • Python密码学库之pycryptodome使用详解
  • QT——信号与槽
  • Git推送代码冲突与Git分支管理
  • reasense api 文档
  • 九、官方人格提示词汇总(中-2)
  • 扩散模型的数学基础 —— 贝叶斯
  • 【LeetCode240.搜索二维矩阵Ⅱ】以及变式
  • ASP.NET Core中数据绑定原理实现详解
  • C++-多态