MyBatis 一对多关联映射详解

MyBatis 一对多关联映射详解

一、概述

一对多(One-to-Many)是数据库设计中最常见的关联关系之一。例如:一个班级(Clazz)包含多个学生(Student) 。在 MyBatis 中,一对多映射使用 <collection> 标签来实现。

1.1 一对多 vs 多对一

对比维度 多对一 一对多
视角 从"多"方看向"一"方 从"一"方看向"多"方
实体类属性 private Clazz clazz; private List<Student> students;
XML 标签 <association> <collection>
标签属性 javaType="Clazz" ofType="Student"
典型场景 学生 → 所属班级 班级 → 学生列表
外键所在表 在"多"方表中(t_student.cid 在"多"方表中(t_student.cid

核心记忆<association> 是"有一个"(Has-A),<collection> 是"有多个"(Has-Many)。

二、场景与实体类

2.1 数据库表结构

sql 复制代码
-- 班级表(一方)
CREATE TABLE t_clazz (
    cid INT PRIMARY KEY AUTO_INCREMENT,
    cname VARCHAR(50),
    location VARCHAR(100)
);

-- 学生表(多方)
CREATE TABLE t_student (
    sid INT PRIMARY KEY AUTO_INCREMENT,
    sname VARCHAR(50),
    age INT,
    cid INT,
    FOREIGN KEY (cid) REFERENCES t_clazz(cid)
);

-- 示例数据
INSERT INTO t_clazz VALUES (1, '高三1班', 'A栋301');
INSERT INTO t_clazz VALUES (2, '高三2班', 'A栋302');

INSERT INTO t_student VALUES (1, '张三', 18, 1);
INSERT INTO t_student VALUES (2, '李四', 18, 1);
INSERT INTO t_student VALUES (3, '王五', 19, 1);
INSERT INTO t_student VALUES (4, '赵六', 18, 2);

2.2 Java 实体类

java 复制代码
// 学生实体(多方)
public class Student {
    private Integer sid;
    private String sname;
    private Integer age;
    // 多对一:学生所属班级(可省略,视需求而定)
    private Clazz clazz;
    // getter/setter
}

// 班级实体(一方)
public class Clazz {
    private Integer cid;
    private String cname;
    private String location;
    private List<Student> students;  // 一对多:班级包含多个学生
    // getter/setter
}

三、两种映射方式详解

3.1 方式一:嵌套结果(JOIN 查询,强烈推荐)

使用一次 LEFT JOIN 查询,一次性获取班级及其所有学生信息,性能最优

XML 配置
xml 复制代码
<resultMap id="ClazzStudentMap" type="Clazz">
    <!-- 班级字段映射 -->
    <id property="cid" column="cid"/>
    <result property="cname" column="cname"/>
    <result property="location" column="location"/>
    
    <!-- 一对多:班级包含多个学生 -->
    <collection property="students" ofType="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <result property="age" column="age"/>
    </collection>
</resultMap>

<select id="selectClazzWithStudents" resultMap="ClazzStudentMap">
    SELECT 
        c.cid,
        c.cname,
        c.location,
        s.sid,
        s.sname,
        s.age
    FROM t_clazz c
    LEFT JOIN t_student s ON c.cid = s.cid
    WHERE c.cid = #{cid}
</select>
生成的 SQL
sql 复制代码
SELECT c.cid, c.cname, c.location,
       s.sid, s.sname, s.age
FROM t_clazz c
LEFT JOIN t_student s ON c.cid = s.cid
WHERE c.cid = 1
执行结果
sql 复制代码
+------+---------+----------+------+-------+------+
| cid  | cname   | location | sid  | sname | age  |
+------+---------+----------+------+-------+------+
| 1    | 高三1班 | A栋301   | 1    | 张三  | 18   |
| 1    | 高三1班 | A栋301   | 2    | 李四  | 18   |
| 1    | 高三1班 | A栋301   | 3    | 王五  | 19   |
+------+---------+----------+------+-------+------+

MyBatis 会自动将 3 行结果合并为 1 个 Clazz 对象,其 students 列表包含 3 个 Student 对象。

关键点:<id> 标签的作用
xml 复制代码
<id property="cid" column="cid"/>

<id> 标签告诉 MyBatis 使用哪个列来识别对象是否唯一。如果没有 <id>,MyBatis 无法判断哪些行属于同一个班级,会导致数据重复(产生多个 Clazz 对象)。

3.2 方式二:分布查询(嵌套查询 + 延迟加载)

先查班级,再通过子查询查该班级的学生列表。

XML 配置
xml 复制代码
<resultMap id="ClazzStudentLazyMap" type="Clazz">
    <id property="cid" column="cid"/>
    <result property="cname" column="cname"/>
    <result property="location" column="location"/>
    
    <!-- 分布查询:通过 StudentMapper.selectByCid 查询学生列表 -->
    <collection property="students" 
                ofType="Student"
                select="com.xie.mapper.StudentMapper.selectByCid"
                column="cid"
                fetchType="lazy"/>
</resultMap>

<select id="selectClazzLazy" resultMap="ClazzStudentLazyMap">
    SELECT cid, cname, location FROM t_clazz WHERE cid = #{cid}
</select>

对应的 StudentMapper

xml 复制代码
<select id="selectByCid" resultType="Student">
    SELECT sid, sname, age FROM t_student WHERE cid = #{cid}
</select>
调用与执行流程
java 复制代码
Clazz clazz = clazzMapper.selectClazzLazy(1);
// 此时只执行了:SELECT cid, cname, location FROM t_clazz WHERE cid = 1
    
System.out.println("班级名称:" + clazz.getCname());
// 访问 students 列表时触发延迟加载
System.out.println("学生人数:" + clazz.getStudents().size());
// 此时执行:SELECT sid, sname, age FROM t_student WHERE cid = 1

四、<collection> 标签属性详解

属性 作用 示例
property 实体类中集合属性的名称 students
ofType 集合中元素的类型(必须 Student
javaType 集合的类型(默认 List,一般无需配置) java.util.ArrayList
select 分布查询时,指定子查询的 SQL ID com.xie.mapper.StudentMapper.selectByCid
column 传递给子查询的列名或参数 cid{param1=cid, param2=age}
fetchType 加载时机:lazy(延迟)或 eager(立即) lazy
resultMap 引用外部的 resultMap studentResultMap

五、延迟加载配置

5.1 全局配置(mybatis-config.xml

xml 复制代码
<settings>
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭激进加载(推荐) -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

5.2 局部配置(覆盖全局)

xml 复制代码
<!-- 延迟加载 -->
<collection property="students" 
            select="com.xie.mapper.StudentMapper.selectByCid"
            column="cid"
            fetchType="lazy"/>

<!-- 立即加载(即使全局开启延迟) -->
<collection property="students" 
            select="com.xie.mapper.StudentMapper.selectByCid"
            column="cid"
            fetchType="eager"/>

六、N+1 查询问题与优化

6.1 问题描述

使用分布查询时,查询 N 个班级会执行 1 + N 次 SQL:

java 复制代码
List<Clazz> clazzes = clazzMapper.selectAll(); // 1 次 SQL
for (Clazz c : clazzes) {
    // 每个班级的 getStudents() 都会触发 1 次查询
    System.out.println(c.getStudents().size()); // N 次 SQL
}

6.2 解决方案:使用 JOIN 查询

xml 复制代码
<select id="selectAll" resultMap="ClazzStudentMap">
    SELECT c.*, s.*
    FROM t_clazz c
    LEFT JOIN t_student s ON c.cid = s.cid
</select>

此时只需 1 次 SQL,彻底解决 N+1 问题。

七、级联操作(Cascade)

7.1 插入班级时顺便插入学生

xml 复制代码
<insert id="insertClazzWithStudents" useGeneratedKeys="true" keyProperty="cid">
    INSERT INTO t_clazz (cname, location) VALUES (#{cname}, #{location})
</insert>

<insert id="insertStudents">
    INSERT INTO t_student (sname, age, cid) VALUES
    <foreach collection="list" item="s" separator=",">
        (#{s.sname}, #{s.age}, #{s.clazz.cid})
    </foreach>
</insert>

八、常见错误与解决方案

8.1 ❌ collection 使用 javaType 而非 ofType

xml 复制代码
<!-- 错误 -->
<collection property="students" javaType="List">

<!-- 正确 -->
<collection property="students" ofType="Student">

8.2 ❌ 忘记配置 <id> 导致数据重复

xml 复制代码
<!-- 错误:缺少 id,MyBatis 无法去重 -->
<resultMap id="ClazzMap" type="Clazz">
    <result property="cname" column="cname"/>
    <collection property="students" ofType="Student">
        <result property="sname" column="sname"/>
    </collection>
</resultMap>

<!-- 正确:配置 id -->
<resultMap id="ClazzMap" type="Clazz">
    <id property="cid" column="cid"/>
    <result property="cname" column="cname"/>
    <collection property="students" ofType="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
    </collection>
</resultMap>

8.3 ❌ 列名冲突(多表 JOIN 时)

sql 复制代码
-- 错误:两表都有 id,覆盖
SELECT c.*, s.* FROM t_clazz c LEFT JOIN t_student s ON c.cid = s.cid

-- 正确:使用别名
SELECT c.cid AS cid, s.sid AS sid, ...

九、最佳实践总结

场景 推荐方案 理由
普通查询(数据量适中) 嵌套结果(JOIN) 一次查询,性能最佳
关联数据访问频率低 分布查询 + 延迟加载 按需加载,节省资源
主表数据量巨大 分布查询 + 分页 避免一次性加载过多数据
多层嵌套关联 嵌套结果(JOIN) 避免 N+1 问题
代码复用性要求高 分布查询 子查询可独立复用

结语

<collection> 是 MyBatis 中处理一对多关联的核心标签。掌握它需要理解以下几点:

  1. ofType 属性 :指定集合中元素的类型,不能写成 javaType
  2. <id> 标签 :在 resultMap 中配置主键,避免数据重复。
  3. JOIN 查询优先:嵌套结果(JOIN)性能最优,应作为默认选择。
  4. 延迟加载按需使用:仅在关联数据访问频率低时使用,注意避免 N+1 问题。

通过合理选择映射方式,可以轻松应对各种复杂的一对多关联场景,写出高性能、可维护的代码。

相关推荐
阿弱1 小时前
pi 扩展机制:加载、执行与能力
后端·llm·agent
用户3126874877201 小时前
线程池到底怎么调?从源码看懂 ThreadPoolExecutor 的 7 个参数
java
SQL-First布道者1 小时前
持久层框架的评价标准:只有一个
java·spring boot·spring·tomcat·mybatis·spring jdbc
孔明click331 小时前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·sa-token·开源·springboot·权限认证
m0_587383002 小时前
智慧场馆解决方案实战指南:从系统架构到落地部署全解析
java·spring boot·架构·系统架构
ZC跨境爬虫2 小时前
LeetCode 13. 罗马数字转整数(多解法详解 + Java Python 实现)
java·python·leetcode
省长2 小时前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·后端·开源
tech_zjf2 小时前
当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
前端·后端
LayZhangStrive2 小时前
融360 一面
java·面试·后端开发