JAVA:MyBatis 如何处理延迟加载的技术指南

admin
10
2025-08-20

1、简述

在实际业务中,我们经常会遇到复杂的实体关系:

🔹 一个用户(User)对应多个订单(Orders)
🔹 一个订单又包含多个商品(Items)

如果一次性把所有数据都查询出来,既浪费资源,又会导致 N+1 查询问题
MyBatis 提供了 延迟加载(Lazy Loading) 机制,能让我们在真正使用关联对象时才去查询,从而提高性能。

image-mm2j.png


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 问题)。

🔹 如果后续一定会用到关联数据,可以使用 嵌套查询 + 立即加载

动物装饰