引言
在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标签)
比如User
和Order
的一对多关系,用<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标签)
比如Order
和User
的多对一关系,用<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
中设置proxyFactory
为CGLIB
,并配合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 {
@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
改为立即加载)。
五、总结:延迟加载的最佳实践
- 按需使用:高频访问的小数据量关联对象(如用户的姓名、手机号)可以立即加载;低频访问的大数据量关联对象(如用户的订单列表)用延迟加载。
- 避免N+1:用
@BatchSize
或CGLIB
批量加载优化,减少查询次数。 - 事务兜底:所有延迟加载操作必须在事务中执行(Spring的
@Transactional
是神器)。 - 监控SQL:通过MyBatis日志(
logImpl=STDOUT_LOGGING
)或APM工具(如SkyWalking)监控查询次数,及时发现性能瓶颈。
最后提醒:延迟加载不是“银弹”,滥用可能导致复杂度上升。结合业务场景选择合适的加载策略(立即加载/延迟加载),才能让系统性能最大化!
如果觉得本文对你有帮助,欢迎点赞收藏,评论区一起交流~ 😊