MyBatis 多对一分布查询与延迟加载的实现
作者:步行cgn
在 MyBatis 中,处理多对一关联关系有两种主流方式:嵌套结果(JOIN 查询) 和 分布查询(嵌套查询 + 延迟加载)。
本文将重点讲解 分布查询 的概念、配置方式以及 延迟加载 的底层原理,帮助你深入理解 MyBatis 的按需加载机制。
一、什么是分步查询(分步查询)?
分布查询是指:将一次复杂的多表关联查询,拆分为多条简单的单表查询,然后通过 MyBatis 的 select 属性将多次查询的结果按需组合。
1.1 分布查询的核心思想
传统 JOIN 查询: SELECT * FROM t_student s LEFT JOIN t_clazz c ON s.cid = c.cid 分布查询: 步骤1:SELECT * FROM t_student WHERE sid = 1 (查学生) 步骤2:SELECT * FROM t_clazz WHERE cid = 1001 (查班级,根据步骤1的 cid)
1.2 分布查询在 XML 中的配置
<resultMap id="StudentLazyMap" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!-- 分布查询:先查学生,再通过另一个查询查班级 -->
<association property="clazz"
javaType="Clazz"
select="com.xie.mapper.ClazzMapper.selectById"
column="cid"
fetchType="lazy"/>
</resultMap>
<select id="selectBySid" resultMap="StudentLazyMap">
SELECT sid, sname, cid FROM t_student WHERE sid = #{sid}
</select>关键属性说明:
| 属性 | 作用 | 示例 |
|---|---|---|
| select | 指定后续查询的 SQL ID(全限定名) | com.xie.mapper.ClazzMapper.selectById |
| column | 传递给后续查询的列名或参数 | cid(将当前查询结果的 cid 作为参数传给 selectById) |
| fetchType | 控制加载时机:lazy(延迟)或 eager(立即) | lazy |
二、延迟加载(Lazy Loading)核心原理
2.1 什么是延迟加载?
延迟加载是指:MyBatis 只有在真正需要使用关联对象时,才去执行额外的 SQL 查询。
在上面的示例中:
- selectBySid 只查询了学生表,返回的 Student 对象中的 clazz 属性为 null(或代理对象)。
- 当调用 student.getClazz() 时,MyBatis 才执行 ClazzMapper.selectById 查询班级信息,并自动填充到 clazz 属性中。
2.2 底层实现原理
- 代理对象:MyBatis 为 Student 对象生成一个代理(使用 CGLIB 或 Javassist),代理对象会拦截对 clazz 属性的访问。
- 触发条件:当调用 getClazz() 方法时,代理检查该属性是否已被加载。如果尚未加载,则触发 SQL 查询。
- 查询执行:根据 select 属性指定的 SQL ID 和 column 传递的参数,执行查询并填充结果。
- 返回结果:将查询到的对象返回,后续再次调用 getClazz() 时直接返回已加载的对象(不再查库)。
2.3 配置方式
全局配置(mybatis-config.xml)
<settings>
<!-- 开启延迟加载(默认 false) -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭激进加载(默认 false,但建议显式配置) -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 触发延迟加载的方法列表(默认:equals,clone,hashCode,toString) -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>局部配置(覆盖全局)
在 <association> 或 <collection> 中通过 fetchType 覆盖:
<association property="clazz"
select="com.xie.mapper.ClazzMapper.selectById"
column="cid"
fetchType="lazy"/> <!-- 即使全局关闭,此关联仍然延迟 -->
2.4 延迟加载 vs 立即加载
| 比较维度 | 延迟加载(lazy) | 立即加载(eager) |
|---|---|---|
| 执行时机 | 访问关联属性时才执行 | 主查询执行后立即执行 |
| SQL 次数 | 按需执行,可能减少 SQL | 总是执行,SQL 次数固定 |
| 适用场景 | 关联数据访问频率低 | 关联数据几乎总是需要 |
| 性能风险 | 可能引发 N+1 问题 | 可能加载大量无用数据 |
| 配置方式 | fetchType="lazy" | fetchType="eager"(默认) |
三、分布查询的完整示例
3.1 数据库表结构
-- 班级表(一方)
CREATE TABLE t_clazz (
cid INT PRIMARY KEY,
cname VARCHAR(50)
);
-- 学生表(多方)
CREATE TABLE t_student (
sid INT PRIMARY KEY,
sname VARCHAR(50),
cid INT,
FOREIGN KEY (cid) REFERENCES t_clazz(cid)
);3.2 实体类
public class Clazz {
private Integer cid;
private String cname;
// getter/setter
}
public class Student {
private Integer sid;
private String sname;
private Clazz clazz; // 多对一关联
// getter/setter
}
3.3 Mapper 接口
public interface ClazzMapper {
Clazz selectById(@Param("cid") Integer cid);
}
public interface StudentMapper {
Student selectBySid(@Param("sid") Integer sid);
}
3.4 XML 配置
ClazzMapper.xml
<select id="selectById" resultType="Clazz">
SELECT * FROM t_clazz WHERE cid = #{cid}
</select>StudentMapper.xml
<resultMap id="StudentClazzMap" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!-- 分布查询:通过 ClazzMapper.selectById 加载班级 -->
<association property="clazz"
javaType="Clazz"
select="com.xie.mapper.ClazzMapper.selectById"
column="cid"
fetchType="lazy"/>
</resultMap>
<select id="selectBySid" resultMap="StudentClazzMap">
SELECT sid, sname, cid FROM t_student WHERE sid = #{sid}
</select>3.5 调用与执行流程
@Test
public void testLazyLoading() {
Student student = studentMapper.selectBySid(1);
// 此时只执行了:SELECT * FROM t_student WHERE sid = 1
// student.getClazz() 尚未执行 SQL
System.out.println("学生姓名:" + student.getSname());
// 此处访问 clazz,触发延迟加载
System.out.println("班级名称:" + student.getClazz().getCname());
// 此时执行:SELECT * FROM t_clazz WHERE cid = 1001
}
控制台日志:
==> Preparing: SELECT sid, sname, cid FROM t_student WHERE sid = ? ==> Parameters: 1(Integer) <== Total: 1 学生姓名:张三 ==> Preparing: SELECT * FROM t_clazz WHERE cid = ? ==> Parameters: 1001(Integer) <== Total: 1 班级名称:高三1班
四、N+1 查询问题详解
4.1 什么是 N+1 问题?
当使用分布查询查询多条主记录时,可能产生 N+1 次 SQL 查询。
示例:查询所有学生及其班级
List<Student> students = studentMapper.selectAll(); // 1 次 SQL
for (Student s : students) {
// 每个 student.getClazz() 都会触发 1 次 SQL
System.out.println(s.getClazz().getCname());
}
如果 students 有 10 条记录,总 SQL 次数为:1(主查询) + 10(每个学生查班级) = 11 次。
4.2 如何避免 N+1 问题?
方案一:使用嵌套结果(JOIN 查询)—— 推荐
<resultMap id="StudentClazzJoinMap" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<association property="clazz" javaType="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
</association>
</resultMap>
<select id="selectAll" resultMap="StudentClazzJoinMap">
SELECT s.sid, s.sname, c.cid, c.cname
FROM t_student s
LEFT JOIN t_clazz c ON s.cid = c.cid
</select>此时只有 1 次 SQL,性能最优。
方案二:批量加载(利用foreach或IN查询)
将 N+1 次子查询合并为 1 次 IN 查询,但需要手动处理。MyBatis 本身不直接支持,可借助自定义插件或外部工具。
五、常见陷阱与解决方案
5.1 延迟加载在 Session 关闭后失效
现象:
Student student = studentMapper.selectBySid(1); sqlSession.close(); // 关闭 Session System.out.println(student.getClazz().getCname()); // 抛出 LazyInitializationException
原因:SqlSession 关闭后,无法执行延迟加载的 SQL。
解决方案:
- 在 Service 层使用 @Transactional,确保在事务中完成所有加载。
- 在需要延迟加载的场景,保持 SqlSession 打开到业务结束。
- 使用 Spring 的 OpenSessionInViewFilter(OSIV)模式。
5.2 延迟加载被意外触发
如果调用 toString()、hashCode()、equals() 等方法,可能触发延迟加载。
<settings>
<!-- 配置哪些方法不会触发延迟加载 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>5.3 分布查询的列传参
当需要传递多个参数时,使用 column="{key1=col1, key2=col2}"。
<association property="clazz"
select="com.xie.mapper.ClazzMapper.selectByIdAndName"
column="{cid=cid, cname=name}"
fetchType="lazy"/>对应的子查询方法:
Clazz selectByIdAndName(@Param("cid") Integer cid, @Param("cname") String cname);
六、分布查询 vs 嵌套结果 总结
| 对比维度 | 分布查询(嵌套查询 + 延迟加载) | 嵌套结果(JOIN 查询) |
|---|---|---|
| SQL 条数 | 可能多(有 N+1 风险) | 只有 1 条 |
| 性能 | 按需加载,可能更省带宽 | 一次性加载,数据量大时内存占用高 |
| 代码复用 | 子查询可以独立使用,复用性高 | SQL 混合在一起,复用性低 |
| 适用场景 | 关联数据访问频率低,主表数据量大 | 关联数据经常访问,表数据量适中 |
| 配置复杂度 | 较高(需配置 select 和 column) | 较低(只需写好 JOIN) |
七、最佳实践建议
- 默认使用嵌套结果(JOIN 查询):性能最稳定,避免 N+1 问题。
- 分布查询 + 延迟加载 用于特定场景:
- 关联数据访问频率很低(如用户信息中的头像、签名等)。
- 主表数据量巨大,且关联表数据几乎用不到时。
- 避免在大列表循环中触发延迟加载:否则会放大 N+1 问题。
- 合理配置全局与局部 fetchType:全局开启延迟加载,局部用 fetchType="eager" 覆盖需要立即加载的关联。
- 在 Spring 项目中注意 SqlSession 生命周期,避免 LazyInitializationException。
结语
分布查询与延迟加载是 MyBatis 提供的强大功能,它们让开发者能够在“高性能(JOIN)”和“轻量级(按需加载)”之间灵活平衡。
- 分布查询(分步查询):将复杂拆分为简单,提升代码复用性。
- 延迟加载(Lazy Loading):按需执行,避免资源浪费。
- N+1 问题:是使用延迟加载时必须警惕的陷阱,需结合业务场景合理选择。
掌握这些技术,能让你在面对复杂关联查询时游刃有余,写出高性能、易维护的代码。
到此这篇关于MyBatis 多对一分布查询与延迟加载的实现的文章就介绍到这了,更多相关MyBatis 多对一分布查询与延迟加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
