副标题:懒加载的智慧,按需获取数据!🎯
🎬 开场:什么是延迟加载?
立即加载 vs 延迟加载
场景:查询订单信息
立即加载(Eager Loading):
查询订单时,立即查询关联的用户信息
┌──────────────────────────────┐
│ SELECT * FROM orders WHERE id = 1
│ SELECT * FROM users WHERE id = order.user_id ← 立即执行
└──────────────────────────────┘
问题:
- 如果不需要用户信息,也会查询
- 浪费资源 ❌
延迟加载(Lazy Loading):
查询订单时,不查询用户信息
只有在访问 order.getUser() 时,才查询用户
┌──────────────────────────────┐
│ SELECT * FROM orders WHERE id = 1 ← 只查订单
│
│ order.getUser() ← 访问时才查询
│ SELECT * FROM users WHERE id = order.user_id
└──────────────────────────────┘
优点:
- 按需加载
- 节省资源 ✅
📚 延迟加载配置
全局配置
<!-- mybatis-config.xml -->
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 积极加载:false表示按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
局部配置
<!-- Mapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<!-- association:一对一关联 -->
<resultMap id="OrderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
<!-- 延迟加载用户信息 -->
<association
property="user"
column="user_id"
select="com.example.mapper.UserMapper.selectById"
fetchType="lazy"/> <!-- lazy:延迟加载 -->
</resultMap>
<select id="selectById" resultMap="OrderResultMap">
SELECT * FROM orders WHERE id = #{id}
</select>
</mapper>
<!-- collection:一对多关联 -->
<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 延迟加载订单列表 -->
<collection
property="orders"
column="id"
select="com.example.mapper.OrderMapper.selectByUserId"
fetchType="lazy"/> <!-- lazy:延迟加载 -->
</resultMap>
<select id="selectById" resultMap="UserResultMap">
SELECT * FROM users WHERE id = #{id}
</select>
🎯 延迟加载原理
代理对象
/**
* MyBatis延迟加载原理:动态代理
*/
// 1. 查询订单
Order order = orderMapper.selectById(1L);
// 此时order.user是一个代理对象(不是真实的User对象)
System.out.println("订单ID:" + order.getId()); // 正常访问
System.out.println("订单金额:" + order.getAmount()); // 正常访问
// 2. 访问user属性,触发延迟加载
User user = order.getUser();
// ↑ 此时才执行:SELECT * FROM users WHERE id = ?
System.out.println("用户名:" + user.getName());
代理实现方式
MyBatis支持两种代理方式:
1. CGLIB(默认)
- 字节码增强
- 创建目标类的子类
- 性能好
2. JAVASSIST
- 字节码操作库
- 运行时生成代理类
- 体积小
配置代理方式
<settings>
<!-- 指定代理方式:CGLIB 或 JAVASSIST -->
<setting name="proxyFactory" value="CGLIB"/>
</settings>
🔍 源码分析
核心类
/**
* 延迟加载的核心实现
*
* 源码位置:ResultLoaderMap
*/
public class ResultLoaderMap {
// 存储延迟加载的信息
private final Map<String, LoadPair> loaderMap = new HashMap<>();
/**
* 添加延迟加载信息
*/
public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("...");
}
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
}
/**
* 加载属性
*/
public boolean load(String property) throws SQLException {
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
pair.load(); // 触发查询
return true;
}
return false;
}
/**
* 加载所有延迟加载的属性
*/
public void loadAll() throws SQLException {
final Set<String> methodNameSet = loaderMap.keySet();
String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
for (String methodName : methodNames) {
load(methodName);
}
}
/**
* 加载配对
*/
private static class LoadPair implements Serializable {
private static final long serialVersionUID = 20130412;
private transient MetaObject metaResultObject;
private transient ResultLoader resultLoader;
private transient Log log;
private final String property;
private final String uppercaseProperty;
private final Class<?> targetType;
public void load() throws SQLException {
if (this.metaResultObject == null) {
throw new IllegalArgumentException("...");
}
if (this.resultLoader == null) {
throw new IllegalArgumentException("...");
}
// 执行查询
this.load(null);
}
public void load(Object userObject) throws SQLException {
// 加载结果
List<Object> list = selectList(userObject);
Object value = resultExtractor.extractObjectFromList(list, targetType);
// 设置属性值
metaResultObject.setValue(property, value);
}
}
}
CGLIB代理
/**
* CGLIB代理实现
*
* 源码位置:CglibProxyFactory
*/
public class CglibProxyFactory implements ProxyFactory {
@Override
public Object createProxy(Object target, ResultLoaderMap lazyLoader,
Configuration configuration, ObjectFactory objectFactory,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
return EnhancedResultObjectProxyImpl.createProxy(
target, lazyLoader, configuration, objectFactory,
constructorArgTypes, constructorArgs
);
}
/**
* 代理实现
*/
private static class EnhancedResultObjectProxyImpl implements MethodInterceptor {
private final Object target;
private final ResultLoaderMap lazyLoader;
@Override
public Object intercept(Object enhanced, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
final String methodName = method.getName();
try {
// 如果是getter方法
if (lazyLoader.size() > 0 && !"equals".equals(methodName)) {
// 判断是否需要加载所有属性
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isGetter(methodName)) {
// 获取属性名
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
// 加载该属性
lazyLoader.load(property);
}
}
}
// 调用原方法
return methodProxy.invokeSuper(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
}
💻 完整示例
实体类
/**
* 订单实体
*/
public class Order implements Serializable {
private Long id;
private String orderNo;
private BigDecimal amount;
private Long userId;
// 关联的用户(延迟加载)
private User user;
// getter/setter...
}
/**
* 用户实体
*/
public class User implements Serializable {
private Long id;
private String name;
private String email;
// 关联的订单列表(延迟加载)
private List<Order> orders;
// getter/setter...
}
Mapper接口
/**
* 订单Mapper
*/
@Mapper
public interface OrderMapper {
/**
* 根据ID查询订单(带延迟加载)
*/
Order selectById(Long id);
/**
* 根据用户ID查询订单列表
*/
List<Order> selectByUserId(Long userId);
}
/**
* 用户Mapper
*/
@Mapper
public interface UserMapper {
/**
* 根据ID查询用户(带延迟加载)
*/
User selectById(Long id);
}
Mapper XML
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 结果映射(延迟加载用户) -->
<resultMap id="OrderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="amount" column="amount"/>
<result property="userId" column="user_id"/>
<!-- 延迟加载用户信息 -->
<association
property="user"
column="user_id"
select="com.example.mapper.UserMapper.selectById"
fetchType="lazy"/>
</resultMap>
<select id="selectById" resultMap="OrderResultMap">
SELECT * FROM orders WHERE id = #{id}
</select>
<select id="selectByUserId" resultType="Order">
SELECT * FROM orders WHERE user_id = #{userId}
</select>
</mapper>
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 结果映射(延迟加载订单列表) -->
<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<!-- 延迟加载订单列表 -->
<collection
property="orders"
column="id"
select="com.example.mapper.OrderMapper.selectByUserId"
fetchType="lazy"/>
</resultMap>
<select id="selectById" resultMap="UserResultMap">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
测试代码
/**
* 延迟加载测试
*/
@SpringBootTest
public class LazyLoadingTest {
@Autowired
private OrderMapper orderMapper;
@Test
public void testLazyLoading() {
System.out.println("========== 1. 查询订单 ==========");
Order order = orderMapper.selectById(1L);
// SQL: SELECT * FROM orders WHERE id = 1
System.out.println("订单ID:" + order.getId());
System.out.println("订单号:" + order.getOrderNo());
System.out.println("订单金额:" + order.getAmount());
System.out.println("\n========== 2. 访问用户(触发延迟加载)==========");
User user = order.getUser();
// SQL: SELECT * FROM users WHERE id = ? (此时才执行)
System.out.println("用户名:" + user.getName());
System.out.println("用户邮箱:" + user.getEmail());
System.out.println("\n========== 3. 再次访问用户(不会再查询)==========");
User user2 = order.getUser();
System.out.println("用户名:" + user2.getName()); // 不会再执行SQL
System.out.println("是否同一个对象:" + (user == user2)); // true
}
@Test
public void testLazyLoadingCollection() {
System.out.println("========== 1. 查询用户 ==========");
User user = userMapper.selectById(1L);
// SQL: SELECT * FROM users WHERE id = 1
System.out.println("用户ID:" + user.getId());
System.out.println("用户名:" + user.getName());
System.out.println("\n========== 2. 访问订单列表(触发延迟加载)==========");
List<Order> orders = user.getOrders();
// SQL: SELECT * FROM orders WHERE user_id = ? (此时才执行)
System.out.println("订单数量:" + orders.size());
for (Order order : orders) {
System.out.println("订单号:" + order.getOrderNo());
}
}
}
输出结果
========== 1. 查询订单 ==========
==> Preparing: SELECT * FROM orders WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
订单ID:1
订单号:ORD001
订单金额:100.00
========== 2. 访问用户(触发延迟加载)==========
==> Preparing: SELECT * FROM users WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
用户名:张三
用户邮箱:zhangsan@example.com
========== 3. 再次访问用户(不会再查询)==========
用户名:张三
是否同一个对象:true
🚨 延迟加载的坑
坑1:Session关闭后无法加载
/**
* 问题:Session关闭后,延迟加载失败
*/
@Test
public void testLazyLoadingAfterSessionClose() {
Order order;
// SqlSession 1
SqlSession sqlSession1 = sqlSessionFactory.openSession();
OrderMapper mapper1 = sqlSession1.getMapper(OrderMapper.class);
order = mapper1.selectById(1L);
System.out.println("订单ID:" + order.getId());
sqlSession1.close(); // 关闭Session
// 访问user属性
try {
User user = order.getUser(); // 抛异常!
System.out.println("用户名:" + user.getName());
} catch (Exception e) {
System.out.println("错误:" + e.getMessage());
// org.apache.ibatis.executor.ExecutorException:
// Executor was closed
}
}
/**
* 解决方案1:在Session关闭前访问
*/
@Test
public void testSolution1() {
SqlSession sqlSession = sqlSessionFactory.openSession();
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order = mapper.selectById(1L);
// 在关闭前访问
User user = order.getUser(); // ✅
System.out.println("用户名:" + user.getName());
sqlSession.close();
}
/**
* 解决方案2:使用立即加载
*/
<association
property="user"
column="user_id"
select="com.example.mapper.UserMapper.selectById"
fetchType="eager"/> <!-- 立即加载 -->
坑2:序列化问题
/**
* 问题:延迟加载的对象无法序列化
*/
@Test
public void testSerialization() throws Exception {
Order order = orderMapper.selectById(1L);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
try {
oos.writeObject(order); // 可能失败
} catch (Exception e) {
System.out.println("序列化失败:" + e.getMessage());
}
}
/**
* 解决方案:序列化前先加载
*/
@Test
public void testSerializationSolution() throws Exception {
Order order = orderMapper.selectById(1L);
// 先触发加载
order.getUser();
// 再序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(order); // ✅
}
坑3:N+1问题
/**
* 问题:N+1查询问题
*/
@Test
public void testNPlusOneProblem() {
// 查询所有订单
List<Order> orders = orderMapper.selectAll();
// SQL 1: SELECT * FROM orders (1次查询)
// 遍历订单,访问用户
for (Order order : orders) {
User user = order.getUser();
// SQL 2-N: SELECT * FROM users WHERE id = ? (N次查询)
System.out.println(user.getName());
}
// 总共:1 + N 次查询 ❌
}
/**
* 解决方案:使用JOIN查询
*/
<resultMap id="OrderResultMapWithUser" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<!-- 使用JOIN,不使用延迟加载 -->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
</resultMap>
<select id="selectAllWithUser" resultMap="OrderResultMapWithUser">
SELECT
o.*,
u.id as user_id,
u.name as user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
</select>
// 只需要1次查询 ✅
List<Order> orders = orderMapper.selectAllWithUser();
🎯 最佳实践
何时使用延迟加载
✅ 适合使用延迟加载:
- 关联数据不是每次都需要
- 关联数据量大
- 查询性能敏感
- 例如:订单详情(不一定要看用户信息)
❌ 不适合使用延迟加载:
- 关联数据几乎总是需要
- 存在N+1问题
- Session生命周期短
- 例如:用户列表(总是要显示用户名)
配置建议
<!-- 推荐配置 -->
<settings>
<!-- 全局延迟加载:开启 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 积极加载:关闭(按需加载)-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 延迟加载触发方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<!-- 在Mapper中灵活控制 -->
<association fetchType="lazy"/> <!-- 延迟加载 -->
<association fetchType="eager"/> <!-- 立即加载 -->
🎉 总结
核心原理
1. 代理对象
- CGLIB/JAVASSIST动态代理
- 拦截getter方法
2. 触发时机
- 访问关联属性时
- 执行查询SQL
3. 查询缓存
- 加载后缓存在对象中
- 不会重复查询
4. 生命周期
- 依赖SqlSession
- Session关闭后失效
记忆口诀
延迟加载按需查,
不用不查省资源。
association一对一,
collection一对多。
fetchType设为lazy,
延迟加载就开启。
动态代理来实现,
CGLIB字节码增强。
拦截getter方法,
访问时才查询。
三大坑要注意:
Session关闭会失效,
序列化前要加载,
N+1问题要避免。
何时用延迟加载?
数据不总是需要,
数据量大性能敏感,
推荐使用延迟加载。
何时用立即加载?
数据几乎都要用,
避免N+1问题,
推荐使用JOIN查询。
懒加载智慧用,
按需获取性能优!
愿你的查询按需加载,性能优化到极致! ⏱️✨