乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

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 对象模型是树形的。框架需要在内存中完成“行 → 对象 → 集合聚合”的转换。

关键机制说明:

  1. 主对象去重:MyBatis 以 <resultMap> 中主对象的 <id> 列值为键,维护一个对象缓存。相同 class.id 的多行不会重复创建 Class 实例。
  2. 集合聚合:每一行都会创建一个 Student 实例(因为 Student 在 collection 内部,其 <id> 仅用于集合元素去重,不影响主对象复用),并添加到对应 Class 的 students 集合中。
  3. 结果集顺序: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
);

初始数据:

idclass_namedepartment
1软件工程一班计算机学院
2信息安全一班计算机学院
idnameagemajorscoreclass_idmentor_id
1大翔22软件工程89.5011
2白歌21软件工程92.0012
3小崔23信息安全85.0021

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.idclass_namedepartmentstudents(列表内容)
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.ArrayListofType 是集合元素的类型,应写 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 的结果映射分支选择。

上一页
association
下一页
discriminator