⏱️ MyBatis延迟加载:用时再加载!

26 阅读7分钟

副标题:懒加载的智慧,按需获取数据!🎯


🎬 开场:什么是延迟加载?

立即加载 vs 延迟加载

场景:查询订单信息

立即加载(Eager Loading):

查询订单时,立即查询关联的用户信息
┌──────────────────────────────┐
│ SELECT * FROM orders WHERE id = 1SELECT * 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查询。

懒加载智慧用,
按需获取性能优!

愿你的查询按需加载,性能优化到极致! ⏱️✨