java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis 多对一分布查询与延迟加载

MyBatis 多对一分布查询与延迟加载的实现

作者:步行cgn

本文主要介绍了MyBatis 多对一分布查询与延迟加载的实现,包括配置方法、底层代理原理、N+1问题成因及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

在 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 查询。

在上面的示例中:

2.2 底层实现原理

  1. 代理对象:MyBatis 为 Student 对象生成一个代理(使用 CGLIB 或 Javassist),代理对象会拦截对 clazz 属性的访问。
  2. 触发条件:当调用 getClazz() 方法时,代理检查该属性是否已被加载。如果尚未加载,则触发 SQL 查询。
  3. 查询执行:根据 select 属性指定的 SQL ID 和 column 传递的参数,执行查询并填充结果。
  4. 返回结果:将查询到的对象返回,后续再次调用 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。

解决方案:

  1. 在 Service 层使用 @Transactional,确保在事务中完成所有加载。
  2. 在需要延迟加载的场景,保持 SqlSession 打开到业务结束。
  3. 使用 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)

七、最佳实践建议

  1. 默认使用嵌套结果(JOIN 查询):性能最稳定,避免 N+1 问题。
  2. 分布查询 + 延迟加载 用于特定场景:
    • 关联数据访问频率很低(如用户信息中的头像、签名等)。
    • 主表数据量巨大,且关联表数据几乎用不到时。
  3. 避免在大列表循环中触发延迟加载:否则会放大 N+1 问题。
  4. 合理配置全局与局部 fetchType:全局开启延迟加载,局部用 fetchType="eager" 覆盖需要立即加载的关联。
  5. 在 Spring 项目中注意 SqlSession 生命周期,避免 LazyInitializationException。

结语

分布查询与延迟加载是 MyBatis 提供的强大功能,它们让开发者能够在“高性能(JOIN)”和“轻量级(按需加载)”之间灵活平衡。

掌握这些技术,能让你在面对复杂关联查询时游刃有余,写出高性能、易维护的代码。

到此这篇关于MyBatis 多对一分布查询与延迟加载的实现的文章就介绍到这了,更多相关MyBatis 多对一分布查询与延迟加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文