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 问题)。
🔹 如果后续一定会用到关联数据,可以使用 嵌套查询 + 立即加载。