1、简述
在实际业务中,我们经常会遇到复杂的实体关系:
🔹  一个用户(User)对应多个订单(Orders)
🔹  一个订单又包含多个商品(Items)
如果一次性把所有数据都查询出来,既浪费资源,又会导致 N+1 查询问题。
MyBatis 提供了 延迟加载(Lazy Loading) 机制,能让我们在真正使用关联对象时才去查询,从而提高性能。

2、延迟加载的原理
MyBatis 延迟加载依赖于 动态代理 和 CGLIB 机制。
🔹  当 Mapper 查询主对象(例如 User)时,MyBatis 不会立刻加载关联对象(Orders)。
🔹  关联属性(orders)会被代理对象占位。
🔹  当调用 user.getOrders() 方法时,代理对象才会执行对应的 SQL 查询,完成延迟加载。
在 MyBatis 的配置文件 mybatis-config.xml 中配置:
<settings>
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 按需加载,调用时才触发 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
解释:
🔹  lazyLoadingEnabled=true:全局开启延迟加载
🔹  aggressiveLazyLoading=false:只有真正调用关联对象时才触发 SQL(否则默认会一次性全部加载)
3、实践样例
3.1 数据库表
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50)
);
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT,
  order_no VARCHAR(50),
  FOREIGN KEY (user_id) REFERENCES users(id)
);
初始化数据:
INSERT INTO users(username) VALUES ('Alice'), ('Bob');
INSERT INTO orders(user_id, order_no) VALUES 
(1, 'ORDER_001'),
(1, 'ORDER_002'),
(2, 'ORDER_003');
3.2 实体类
public class User {
    private Integer id;
    private String username;
    private List<Order> orders; // 延迟加载的关联对象
    // getter & setter
}
public class Order {
    private Integer id;
    private String orderNo;
    private Integer userId;
    // getter & setter
}
3.3 Mapper 接口
public interface UserMapper {
    User selectUserById(Integer id);
}
3.4 Mapper XML(UserMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="UserMapper">
    <!-- 主查询:只查用户 -->
    <select id="selectUserById" parameterType="int" resultMap="UserWithOrders">
        SELECT id, username FROM users WHERE id = #{id}
    </select>
    <!-- 延迟加载映射 -->
    <resultMap id="UserWithOrders" type="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <!-- 延迟加载 orders -->
        <collection property="orders" ofType="Order"
                    select="selectOrdersByUserId" column="id"/>
    </resultMap>
    <!-- 查询订单 -->
    <select id="selectOrdersByUserId" parameterType="int" resultType="Order">
        SELECT id, order_no AS orderNo, user_id AS userId
        FROM orders WHERE user_id = #{id}
    </select>
</mapper>
关键点:
🔹  <collection> 中 select 指向另一个查询方法,实现 延迟加载
🔹  column="id" 表示把用户的 id 传给 selectOrdersByUserId
3.5 测试代码
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
public class LazyLoadDemo {
    public static void main(String[] args) throws Exception {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = factory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            // 只执行用户查询
            User user = mapper.selectUserById(1);
            System.out.println("用户:" + user.getUsername());
            // orders 尚未加载
            System.out.println("调用 getOrders() 前未触发查询");
            // 真正访问 orders 时触发 SQL
            user.getOrders().forEach(o -> System.out.println("订单:" + o.getOrderNo()));
        }
    }
}
4、运行结果
日志输出(简化版):
==>  Preparing: SELECT id, username FROM users WHERE id = ?
<==    Columns: id, username
<==        Row: 1, Alice
用户:Alice
调用 getOrders() 前未触发查询
==>  Preparing: SELECT id, order_no AS orderNo, user_id AS userId FROM orders WHERE user_id = ?
<==    Columns: id, orderNo, userId
<==        Row: 1, ORDER_001, 1
<==        Row: 2, ORDER_002, 1
订单:ORDER_001
订单:ORDER_002
可以看到:
🔹  第一次只查询 users 表;
🔹  调用 user.getOrders() 时,才执行 orders 的 SQL。
5、总结
延迟加载原理:MyBatis 使用动态代理拦截方法,在访问关联对象时执行 SQL。
配置要点:
   `lazyLoadingEnabled=true`
   `aggressiveLazyLoading=false`
适用场景:一对多、多对一关系数据,避免无效查询,提高性能。
注意事项:
🔹 延迟加载过度使用可能导致频繁 SQL(N+1 问题)。
🔹 如果后续一定会用到关联数据,可以使用 嵌套查询 + 立即加载。