你是否遇到过查询用户信息却意外加载了所有订单数据的情况?MyBatis的延迟加载就是解决这个痛点的神器!本文将带你深入理解这个提升性能的黑科技!

一、什么是延迟加载?为什么需要它?🤔

生活化比喻:

  • 🚗 立即加载:买一辆车,强制附带所有配件(即使你只需要方向盘)
  • 🛋️ 延迟加载:买一辆车,其他配件按需购买(需要时才购买空调/导航)

技术场景:

// 查询用户(包含订单集合)
User user = userDao.findById(1); 

// 立即加载:执行上面代码时,用户+所有订单数据都已加载
// 延迟加载:只加载用户数据,访问订单时才加载订单数据

延迟加载的价值:

场景 立即加载 延迟加载
查询用户基本信息 加载用户+所有订单(浪费) 只加载用户(高效)
查看用户详情 已加载所有数据(响应快) 首次访问关联数据稍慢
系统资源占用 内存消耗大,数据库压力大 资源按需使用,优化整体性能

二、延迟加载核心原理揭秘

1. 整体执行流程

应用程序 MyBatis 代理对象 数据库 查询主对象(用户) 执行主SQL 返回主对象数据 创建代理对象(含关联属性) 返回用户代理对象 访问关联属性(user.getOrders()) 触发关联查询 执行关联SQL 返回关联数据 注入关联数据 返回真实订单数据 应用程序 MyBatis 代理对象 数据库

2. 代理对象生成原理

继承
持有
触发查询
User
+Integer id
+String name
+List<Order> orders
UserProxy
-User target
-MethodHandler handler
+getOrders()
LazyLoader
+load()
MyBatis

关键点

  • 创建主对象时,关联属性用代理对象占位
  • 首次访问代理属性时,触发真实查询
  • 查询结果替换代理对象成为真实数据

三、延迟加载配置实战 🛠️

1. 全局启用延迟加载

<!-- mybatis-config.xml -->
<settings>
    <!-- 启用延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    
    <!-- 按需加载(默认false,2.3.0后默认true) -->
    <setting name="aggressiveLazyLoading" value="false"/>
    
    <!-- 指定触发加载的方法(默认equals,clone,hashCode,toString) -->
    <setting name="lazyLoadTriggerMethods" value=""/>
</settings>

2. 关联映射配置

<!-- UserMapper.xml -->
<resultMap id="userWithOrders" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    
    <!-- 配置订单集合的延迟加载 -->
    <collection 
        property="orders" 
        ofType="Order"
        select="com.example.mapper.OrderMapper.findByUserId"
        column="id"
        fetchType="lazy"/> <!-- 关键! -->
</resultMap>

<select id="findById" resultMap="userWithOrders">
    SELECT * FROM users WHERE id = #{id}
</select>

3. 注解方式配置

public interface UserMapper {
    @Results({
        @Result(property = "id", column = "id"),
        @Result(property = "name", column = "name"),
        @Result(property = "orders", column = "id",
                many = @Many(
                    select = "com.example.mapper.OrderMapper.findByUserId",
                    fetchType = FetchType.LAZY // 延迟加载
                ))
    })
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findByIdWithOrders(int id);
}

四、深度解析代理实现 🔍

1. 代理对象生成代码(简化版)

public class UserProxy extends User {
    private MethodHandler handler;
    
    @Override
    public List<Order> getOrders() {
        // 首次访问时加载真实数据
        if (handler != null) {
            handler.invoke(); // 触发SQL查询
            handler = null; // 清除处理器
        }
        return super.getOrders();
    }
}

// MyBatis创建代理对象
public User createProxy(User user) {
    UserProxy proxy = new UserProxy();
    proxy.setId(user.getId());
    proxy.setName(user.getName());
    proxy.setHandler(new LazyLoader(user.getId()));
    return proxy;
}

2. 加载触发时序图

应用程序 User代理对象 LazyLoader MyBatis 数据库 user.getOrders() 检查是否已加载 执行关联查询 SELECT * FROM orders WHERE user_id=? 返回订单数据 返回Order列表 注入真实数据 直接返回数据 alt [未加载] [已加载] 返回订单列表 应用程序 User代理对象 LazyLoader MyBatis 数据库

五、延迟加载的四种场景

1. 集合延迟加载(最常见)

User user = userMapper.findById(1);
// 此时只查询用户表

System.out.println(user.getOrders()); 
// 触发查询:SELECT * FROM orders WHERE user_id=1

2. 关联对象延迟加载

<resultMap id="orderDetail" type="Order">
    <association 
        property="product" 
        select="com.example.mapper.ProductMapper.findById"
        column="product_id"
        fetchType="lazy"/>
</resultMap>

3. 嵌套查询延迟

User user = userMapper.findById(1);
// 只加载用户

Order firstOrder = user.getOrders().get(0);
// 触发加载所有订单

Product product = firstOrder.getProduct();
// 触发加载商品信息

4. 深度延迟加载

用户
订单列表
订单详情
商品信息
供应商信息

访问路径

  • 访问用户 → 只查用户
  • 访问用户.orders → 查订单
  • 访问用户.orders[0].product → 查商品
  • 访问user.orders[0].product.supplier → 查供应商

六、性能优化最佳实践 🚀

1. 配置策略对比

配置项 效果 适用场景
lazyLoadingEnabled true 启用延迟加载 大多数关联查询场景
aggressiveLazyLoading false 按需加载(访问属性才加载) 推荐默认设置
true 积极加载(访问任何属性即加载所有关联) 不推荐
lazyLoadTriggerMethods “” 自定义触发方法(空表示仅属性访问触发) 精细控制

2. 避免N+1查询问题

问题场景

List<User> users = userMapper.findAll();
for (User user : users) {
    // 每次循环触发一次查询
    System.out.println(user.getOrders().size()); 
}
// 执行1(主查询)+ N(关联查询)次SQL

解决方案

<!-- 使用联接查询一次性加载 -->
<select id="findAllWithOrders" resultMap="userWithOrders">
    SELECT u.*, o.id as order_id, o.total
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
</select>

3. 混合加载策略

<resultMap id="advancedResultMap" type="User">
    <!-- 立即加载基本信息 -->
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    
    <!-- 延迟加载订单 -->
    <collection property="orders" fetchType="lazy" ... />
    
    <!-- 立即加载常用地址 -->
    <association property="primaryAddress" fetchType="eager" ... />
</resultMap>

七、常见问题解决方案

1. 序列化问题

现象:延迟加载对象序列化时报错
原因:代理对象未实现序列化接口
解决

// 实体类实现Serializable
public class User implements Serializable {
    // ...
}

2. 会话关闭问题

现象:访问延迟属性时报Executor was closed
原因:SqlSession已关闭
解决

// 方案1:使用OpenSessionInView模式(Web应用)
// 方案2:在事务内访问所有需要的数据
try (SqlSession session = factory.openSession()) {
    User user = userMapper.findById(1);
    // 在session关闭前访问延迟数据
    user.getOrders().forEach(System.out::println);
}

3. 性能监控

// 检测延迟加载触发次数
public class LazyListener implements ObjectFactory {
    @Override
    public void onLazyLoad(String property) {
        logger.debug("延迟加载触发: " + property);
        // 监控代码...
    }
}

// 配置文件中注册
<objectFactory type="com.example.LazyListener"/>

八、延迟加载 vs 立即加载

特性 延迟加载 立即加载
查询时机 访问属性时触发 主查询时立即加载
SQL数量 1+N(按需触发) 1(或通过join合并)
内存占用 初始占用小 初始占用大
响应速度 首次访问关联数据稍慢 首次响应快
适用场景 关联数据不常用 关联数据总是需要
复杂度 需处理会话生命周期 实现简单

九、总结:延迟加载的正确打开方式

  1. 配置三步走

    启用lazyLoadingEnabled
    关闭aggressiveLazyLoading
    按需配置fetchType
  2. 遵循最佳实践

    • 频繁使用的关联 → 立即加载
    • 大对象/不常用关联 → 延迟加载
    • 循环中使用 → 避免N+1问题
  3. 避坑指南

    • 实体类实现Serializable
    • 保持SqlSession开启直到使用完延迟数据
    • 生产环境监控延迟加载触发频率

💡 性能黄金法则按需加载是优化数据库交互的最高原则!

最后的小测试
aggressiveLazyLoading=true时,调用user.toString()会发生什么?
A) 只加载用户基本信息
B) 加载所有延迟属性
C) 抛出异常

(答案:B - 因为toString是默认的触发方法之一)

掌握延迟加载,让你的应用性能飞起来!现在就去优化你的代码吧~ 🚀

Logo

更多推荐