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 中处理一对多关联的核心标签。掌握它需要理解以下几点:
ofType属性 :指定集合中元素的类型,不能写成javaType。<id>标签 :在resultMap中配置主键,避免数据重复。- JOIN 查询优先:嵌套结果(JOIN)性能最优,应作为默认选择。
- 延迟加载按需使用:仅在关联数据访问频率低时使用,注意避免 N+1 问题。
通过合理选择映射方式,可以轻松应对各种复杂的一对多关联场景,写出高性能、可维护的代码。