collection
导学
本节学习目标:
- 理解
collection解决的核心问题:将数据库一对多关系映射为 Java 对象的集合属性 - 掌握嵌套 Select 方式(
column+select)配置一对多集合 - 掌握嵌套结果映射方式(单次 JOIN)配置一对多集合,理解结果集拆分机制
- 明确
ofType与javaType的区别与用法 - 能够根据数据特征选择加载策略,避免内存膨胀和 N+1 问题
定义
collection 用于映射一对多关联关系。典型场景包括:一个班级包含多名学生、一个课程有多条选课记录、一个用户有多个收货地址。在数据库中,这种关系通过从表的外键表达;在 Java 中,则表现为 List、Set、Collection 等集合类型的属性。
它解决的核心痛点:
- 将 JOIN 查询返回的扁平化结果集,还原为具有层级结构的领域对象
- 支持按需加载子集合(延迟加载),避免主对象未使用集合时产生不必要查询
- 在内存中正确去重主对象,并将多行从表数据聚合到同一集合中
适用位置与核心属性
collection 只能嵌套在 <resultMap> 内部,作为其子元素出现。
<resultMap id="..." type="...">
<collection property="集合属性名" ofType="集合元素类型"
javaType="集合接口/实现类型"
column="外键列" select="嵌套查询语句ID"
fetchType="lazy/eager"
resultMap="嵌套resultMapID"
columnPrefix="列前缀">
<id property="" column=""/>
<result property="" column=""/>
</collection>
</resultMap>
| 属性 | 必填 | 说明 |
|---|---|---|
property | 是 | 主对象中集合属性的名称 |
ofType | 是 | 集合中元素的 Java 类型(如 Student) |
javaType | 否 | 集合本身的类型(如 ArrayList、HashSet),默认根据属性类型推断 |
column | 条件必填 | 嵌套 Select 方式下必填,指定传递给子查询的参数列 |
select | 条件必填 | 嵌套 Select 方式下必填,指定子查询语句的命名空间 + ID |
resultMap | 条件必填 | 嵌套结果映射方式下必填,引用另一个 resultMap 完成集合元素映射 |
fetchType | 否 | 局部覆盖全局延迟加载配置:lazy 或 eager |
columnPrefix | 否 | 为 collection 内部所有列名自动添加前缀 |
ofType 与 javaType 的辨析:
ofType:回答“集合里装的是什么对象”。例如List<Student>的ofType是Student。javaType:回答“集合本身是什么类型”。例如属性声明为List<Student>时,MyBatis 默认推断javaType为ArrayList,通常无需显式配置。如果属性是Set<Student>,则默认推断为HashSet。
核心原理
一对多集合映射流程图(JOIN 结果集拆分)
当使用嵌套结果映射方式查询一对多关系时,MyBatis 面临的核心挑战是:JOIN 查询返回的二维结果集是扁平的,而 Java 对象模型是树形的。框架需要在内存中完成“行 → 对象 → 集合聚合”的转换。
关键机制说明:
- 主对象去重:MyBatis 以
<resultMap>中主对象的<id>列值为键,维护一个对象缓存。相同class.id的多行不会重复创建Class实例。 - 集合聚合:每一行都会创建一个
Student实例(因为Student在collection内部,其<id>仅用于集合元素去重,不影响主对象复用),并添加到对应Class的students集合中。 - 结果集顺序:MyBatis 要求 JOIN 查询的结果集按主对象的
<id>列排序,否则去重逻辑会失效。虽然 MyBatis 内部会处理,但显式ORDER BY主表 ID 是最佳实践。
完整示例
场景说明
乐途公司学生管理系统中,一个班级包含多名学生。我们将演示:
- 嵌套 Select 方式:先查班级,再按
class_id查学生 - 嵌套结果映射方式:单次 JOIN 查询,将结果映射为
Class对象内含List<Student>
操作前的数据库表结构及初始数据
-- 班级表
CREATE TABLE class (
id INT PRIMARY KEY AUTO_INCREMENT,
class_name VARCHAR(20),
department VARCHAR(20)
);
-- 学生表
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2),
class_id INT,
mentor_id INT
);
初始数据:
| id | class_name | department |
|---|---|---|
| 1 | 软件工程一班 | 计算机学院 |
| 2 | 信息安全一班 | 计算机学院 |
| id | name | age | major | score | class_id | mentor_id |
|---|---|---|---|---|---|---|
| 1 | 大翔 | 22 | 软件工程 | 89.50 | 1 | 1 |
| 2 | 白歌 | 21 | 软件工程 | 92.00 | 1 | 2 |
| 3 | 小崔 | 23 | 信息安全 | 85.00 | 2 | 1 |
Java 实体类
package com.flywing.entity;
import java.util.List;
public class Class {
private Integer id;
private String className;
private String department;
private List<Student> students; // 一对多集合
// getter / setter 省略...
}
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
private Integer classId;
private Integer mentorId;
// getter / setter 省略...
}
完整的映射文件片段
<?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="com.flywing.mapper.ClassMapper">
<!-- 学生基础resultMap -->
<resultMap id="StudentBaseMap" type="com.flywing.entity.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
<result property="age" column="s_age"/>
<result property="major" column="s_major"/>
<result property="score" column="s_score"/>
<result property="classId" column="s_class_id"/>
<result property="mentorId" column="s_mentor_id"/>
</resultMap>
<!-- ==================== 方式一:嵌套Select ==================== -->
<resultMap id="ClassWithStudentsBySelect" type="com.flywing.entity.Class">
<id property="id" column="id"/>
<result property="className" column="class_name"/>
<result property="department" column="department"/>
<collection property="students" ofType="com.flywing.entity.Student"
column="id"
select="com.flywing.mapper.StudentMapper.selectByClassId"/>
</resultMap>
<select id="selectAllClassesWithStudentsBySelect" resultMap="ClassWithStudentsBySelect">
SELECT id, class_name, department FROM class
</select>
<!-- 子查询定义在 StudentMapper.xml 中 -->
<select id="selectByClassId" resultMap="StudentBaseMap"
parameterType="int">
SELECT id AS s_id, name AS s_name, age AS s_age,
major AS s_major, score AS s_score,
class_id AS s_class_id, mentor_id AS s_mentor_id
FROM student
WHERE class_id = #{classId}
</select>
<!-- ==================== 方式二:嵌套结果映射 ==================== -->
<resultMap id="ClassWithStudentsByJoin" type="com.flywing.entity.Class">
<id property="id" column="c_id"/>
<result property="className" column="class_name"/>
<result property="department" column="department"/>
<collection property="students" ofType="com.flywing.entity.Student"
resultMap="StudentBaseMap"
columnPrefix="s_"/>
</resultMap>
<select id="selectAllClassesWithStudentsByJoin" resultMap="ClassWithStudentsByJoin">
SELECT
c.id AS c_id,
c.class_name,
c.department,
s.id AS s_id,
s.name AS s_name,
s.age AS s_age,
s.major AS s_major,
s.score AS s_score,
s.class_id AS s_class_id,
s.mentor_id AS s_mentor_id
FROM class c
LEFT JOIN student s ON c.id = s.class_id
ORDER BY c.id
</select>
</mapper>
实际执行结果
方式一:嵌套 Select 查询
查询结果(2 个班级,每个班级含学生列表):
| class.id | class_name | department | students(列表内容) |
|---|---|---|---|
| 1 | 软件工程一班 | 计算机学院 | [大翔, 白歌] |
| 2 | 信息安全一班 | 计算机学院 | [小崔] |
控制台 SQL 输出:
==> Preparing: SELECT id, class_name, department FROM class
==> Parameters:
<== Columns: id, class_name, department
<== Row: 1, 软件工程一班, 计算机学院
====> Preparing: SELECT id AS s_id, name AS s_name, age AS s_age, major AS s_major, score AS s_score, class_id AS s_class_id, mentor_id AS s_mentor_id FROM student WHERE class_id = ?
====> Parameters: 1(Integer)
<==== Columns: s_id, s_name, s_age, s_major, s_score, s_class_id, s_mentor_id
<==== Row: 1, 大翔, 22, 软件工程, 89.50, 1, 1
<==== Row: 2, 白歌, 21, 软件工程, 92.00, 1, 2
<== Row: 2, 信息安全一班, 计算机学院
====> Preparing: SELECT id AS s_id, name AS s_name, age AS s_age, major AS s_major, score AS s_score, class_id AS s_class_id, mentor_id AS s_mentor_id FROM student WHERE class_id = ?
====> Parameters: 2(Integer)
<==== Columns: s_id, s_name, s_age, s_major, s_score, s_class_id, s_mentor_id
<==== Row: 3, 小崔, 23, 信息安全, 85.00, 2, 1
<== Total: 2
分析:主查询 1 次,子查询 2 次(每个班级触发一次)。如果班级数量为 N,则总 SQL 数为 1 + N。
方式二:嵌套结果映射(JOIN 查询)
查询结果与方式一相同。
控制台 SQL 输出:
==> Preparing: SELECT c.id AS c_id, c.class_name, c.department, s.id AS s_id, s.name AS s_name, s.age AS s_age, s.major AS s_major, s.score AS s_score, s.class_id AS s_class_id, s.mentor_id AS s_mentor_id FROM class c LEFT JOIN student s ON c.id = s.class_id ORDER BY c.id
==> Parameters:
<== Columns: c_id, class_name, department, s_id, s_name, s_age, s_major, s_score, s_class_id, s_mentor_id
<== Row: 1, 软件工程一班, 计算机学院, 1, 大翔, 22, 软件工程, 89.50, 1, 1
<== Row: 1, 软件工程一班, 计算机学院, 2, 白歌, 21, 软件工程, 92.00, 1, 2
<== Row: 2, 信息安全一班, 计算机学院, 3, 小崔, 23, 信息安全, 85.00, 2, 1
<== Total: 3
分析:仅执行 1 条 SQL。结果集有 3 行,但 MyBatis 根据 Class 的 <id column="c_id"/> 判断第 1、2 行属于同一个 Class 实例,因此只创建 1 个 Class(id=1),并向其 students 集合追加两个 Student 对象。第 3 行创建 Class(id=2) 并追加 1 个 Student。最终返回 2 个 Class 对象,与预期完全一致。
分析
- 嵌套 Select 的集合加载逻辑与
association类似,但子查询返回的是多条记录,MyBatis 自动将其封装为List。其性能风险同样是 N+1:班级数越多,子查询次数越多。 - 嵌套结果映射 是处理一对多关系的首选方案,尤其在需要批量展示班级及其学生列表时。
ORDER BY c.id确保结果集按主对象 ID 有序,帮助 MyBatis 高效去重和聚合。 columnPrefix在collection中的作用与association完全一致:隔离不同表的列命名空间,避免id、name等通用列名冲突。
易错场景 / 常见误区
| 误区 | 正解 |
|---|---|
ofType 写成集合类型如 java.util.ArrayList | ofType 是集合元素的类型,应写 Student;集合类型由 javaType 或属性声明推断 |
嵌套结果映射时,主对象未配置 <id> | 主对象必须配置 <id>,否则 MyBatis 无法去重,会导致同一班级出现多次,每次只带一个学生 |
| 嵌套结果映射时,结果集未按主对象 ID 排序 | 虽然 MyBatis 能处理无序结果,但有序结果集的去重和聚合效率更高,且某些场景下无序会导致对象拆分错误 |
嵌套 Select 的 column 传递多列时未用逗号分隔 | 多列参数应写为 column="{prop1=col1, prop2=col2}" 格式,单表外键直接写列名即可 |
在 collection 内部又嵌套 collection 形成多级一对多 | 多级嵌套结果映射会导致结果集笛卡尔积膨胀(如班级×学生×课程),建议分步查询或改用嵌套 Select + 延迟加载 |
面试考点
Q1:collection 中的 ofType 和 javaType 有什么区别?如果属性声明为 List<Student>,这两个属性需要怎么配置?
A:
ofType指定集合中元素的类型,必填,此处为Student;javaType指定集合本身的实现类型,可选,MyBatis 会根据属性类型自动推断为ArrayList。因此List<Student>场景下只需配置ofType="Student",javaType通常省略。
Q2:嵌套结果映射方式查询一对多关系时,为什么结果集返回了 5 行,但最终只得到 2 个主对象?
A:因为 MyBatis 使用主对象
<resultMap>中<id>列的值作为对象标识符。结果集中相同主对象 ID 的多行会被合并到同一个 Java 实例中,仅向其collection集合追加元素。这是collection与association在结果集处理上的核心机制。
Q3:嵌套 Select 方式加载 collection 时,如果子查询返回空结果集,主对象的集合属性会是什么状态?
A:MyBatis 会将其初始化为空集合(如空
ArrayList),而不是null。这避免了调用方出现NullPointerException,符合防御式编程原则。如果确实需要null,需在业务层处理或自定义 TypeHandler。
Q4:生产环境中,一个班级有 1000 名学生,应该使用嵌套 Select 还是嵌套结果映射?
A:如果本次业务必须返回全部 1000 名学生,嵌套结果映射(单次 JOIN)更优,因为数据库往返仅一次。但如果学生列表仅在少数场景使用,且数据量巨大,可考虑嵌套 Select + 延迟加载,避免主查询时一次性加载大量关联数据。极端大数据量下,分页查询子集合比一次性加载更实际。
小结
collection 将关系型数据库的扁平 JOIN 结果,还原为 Java 领域模型的树形集合结构。嵌套结果映射方式通过主对象 <id> 去重和内存聚合,实现了单次 SQL 往返完成一对多加载;嵌套 Select 方式则以多次查询换取配置简洁和延迟加载能力。正确选择加载策略,是避免 N+1 问题和内存膨胀的关键。
下一章引子
association 和 collection 解决了固定结构的关联映射,但现实世界存在多态映射需求:同一表中的数据,根据类型列值映射为不同的 Java 子类。下一节将介绍 discriminator 鉴别器,实现类似 Java switch-case 的结果映射分支选择。