association
导学
本节学习目标:
- 理解
association解决的核心问题:将数据库关联关系映射为 Java 对象的一对一引用 - 掌握嵌套 Select 查询(
column+select)的配置方式与执行原理 - 掌握嵌套结果映射(单次 JOIN 查询)的配置方式与执行原理
- 能够根据业务场景选择正确的加载策略,并预判其性能特征
- 理解
fetchType对全局延迟加载配置的局部覆盖作用
定义
association 用于映射一对一关联关系。典型场景包括:学生属于一个班级、员工隶属于一个部门、订单对应一个收货地址。在关系型数据库中,这种关系通常通过外键表达;在 Java 领域模型中,则表现为一个对象持有另一个对象的引用。
它解决的核心痛点:
- 将分散在两张表中的数据,组装成一张具有对象引用的领域模型
- 支持按需加载关联对象(延迟加载),避免一次性查询过多数据
- 支持单次 JOIN 查询完成主对象与关联对象的组装,减少数据库往返
适用位置与核心属性
association 只能嵌套在 <resultMap> 内部,作为其子元素出现。
<resultMap id="..." type="...">
<association property="关联属性名" javaType="关联对象类型"
column="外键列" select="嵌套查询语句ID"
fetchType="lazy/eager"
resultMap="嵌套resultMapID"
columnPrefix="列前缀">
<id property="" column=""/>
<result property="" column=""/>
</association>
</resultMap>
| 属性 | 必填 | 说明 |
|---|---|---|
property | 是 | 主对象中关联属性的名称 |
javaType | 是 | 关联对象的 Java 类型(全限定名或别名) |
column | 条件必填 | 嵌套 Select 方式下必填,指定传递给子查询的参数列(可为多列,逗号分隔) |
select | 条件必填 | 嵌套 Select 方式下必填,指定子查询语句的命名空间 + ID |
resultMap | 条件必填 | 嵌套结果映射方式下必填,引用另一个 resultMap 完成关联对象映射 |
fetchType | 否 | 局部覆盖全局延迟加载配置:lazy(延迟加载)或 eager(立即加载) |
columnPrefix | 否 | 为 association 内部所有列名自动添加前缀,避免 JOIN 列名冲突 |
两种实现方式对比:
| 维度 | 嵌套 Select(column + select) | 嵌套结果映射(resultMap / 内联) |
|---|---|---|
| SQL 执行次数 | 1 + N 次(主查询 + 每条记录的子查询) | 1 次(单次 JOIN) |
| 配置复杂度 | 简单,子查询独立维护 | 稍复杂,需处理列名冲突和结果集拆分 |
| 性能特征 | 数据量小时方便,数据量大时易引发 N+1 | 数据量大时性能更优,但 JOIN 结果集可能膨胀 |
| 延迟加载支持 | 天然支持 | 不支持(单次查询已返回全部数据) |
| 适用场景 | 关联对象使用频率低,或需要延迟加载 | 关联对象必用,或需要一次性批量加载 |
核心原理
一对一关联查询两种方式对比流程图
关键差异:
- 嵌套 Select:MyBatis 先执行主查询,然后对结果集中的每一行,按
column指定的列值作为参数,再次发起子查询。这种方式逻辑清晰,但数据库往返次数随主查询结果行数线性增长。 - 嵌套结果映射:MyBatis 执行一次 JOIN 查询,在内存中根据
<id>列去重,将属于同一主对象的关联列合并到同一个关联对象中。数据库只往返一次,但结果集可能因 JOIN 而包含重复的主对象列。
完整示例
场景说明
乐途公司学生管理系统中,每位学生有一位专属导师(一对一关系)。我们将分别用两种方式实现“查询学生及其导师信息”,并对比 SQL 输出。
操作前的数据库表结构及初始数据
沿用统一表结构:
-- 学生表
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
);
-- 导师表
CREATE TABLE mentor (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
title VARCHAR(20)
);
初始数据:
| 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 |
| id | name | title |
|---|---|---|
| 1 | 黄俪 | 副教授 |
| 2 | 李眉 | 教授 |
Java 实体类
package com.flywing.entity;
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
private Integer classId;
private Integer mentorId;
private Mentor mentor; // 一对一关联
// getter / setter 省略...
}
public class Mentor {
private Integer id;
private String name;
private String title;
// 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.StudentMapper">
<!-- 导师基础resultMap -->
<resultMap id="MentorMap" type="com.flywing.entity.Mentor">
<id property="id" column="mentor_id"/>
<result property="name" column="mentor_name"/>
<result property="title" column="mentor_title"/>
</resultMap>
<!-- 学生基础resultMap -->
<resultMap id="StudentBaseMap" type="com.flywing.entity.Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<result property="major" column="major"/>
<result property="score" column="score"/>
<result property="classId" column="class_id"/>
<result property="mentorId" column="mentor_id"/>
</resultMap>
<!-- ==================== 方式一:嵌套Select ==================== -->
<resultMap id="StudentWithMentorBySelect" type="com.flywing.entity.Student"
extends="StudentBaseMap">
<association property="mentor" javaType="com.flywing.entity.Mentor"
column="mentor_id"
select="com.flywing.mapper.MentorMapper.selectById"/>
</resultMap>
<select id="selectStudentsWithMentorBySelect" resultMap="StudentWithMentorBySelect">
SELECT id, name, age, major, score, class_id, mentor_id
FROM student
</select>
<!-- 子查询定义在 MentorMapper.xml 中 -->
<select id="selectById" resultMap="MentorMap"
parameterType="int">
SELECT id AS mentor_id, name AS mentor_name, title AS mentor_title
FROM mentor
WHERE id = #{id}
</select>
<!-- ==================== 方式二:嵌套结果映射 ==================== -->
<resultMap id="StudentWithMentorByJoin" type="com.flywing.entity.Student"
extends="StudentBaseMap">
<association property="mentor" javaType="com.flywing.entity.Mentor"
resultMap="MentorMap"
columnPrefix="m_"/>
</resultMap>
<select id="selectStudentsWithMentorByJoin" resultMap="StudentWithMentorByJoin">
SELECT
s.id,
s.name,
s.age,
s.major,
s.score,
s.class_id,
s.mentor_id,
m.id AS m_mentor_id,
m.name AS m_mentor_name,
m.title AS m_mentor_title
FROM student s
LEFT JOIN mentor m ON s.mentor_id = m.id
</select>
</mapper>
实际执行结果
方式一:嵌套 Select 查询
查询结果集(3 条学生记录,每条带导师对象):
| id | name | age | major | score | mentor.id | mentor.name | mentor.title |
|---|---|---|---|---|---|---|---|
| 1 | 大翔 | 22 | 软件工程 | 89.50 | 1 | 黄俪 | 副教授 |
| 2 | 白歌 | 21 | 计算机科学 | 92.00 | 2 | 李眉 | 教授 |
| 3 | 小崔 | 23 | 信息安全 | 85.00 | 1 | 黄俪 | 副教授 |
控制台 SQL 输出:
==> Preparing: SELECT id, name, age, major, score, class_id, mentor_id FROM student
==> Parameters:
<== Columns: id, name, age, major, score, class_id, mentor_id
<== Row: 1, 大翔, 22, 软件工程, 89.50, 1, 1
====> Preparing: SELECT id AS mentor_id, name AS mentor_name, title AS mentor_title FROM mentor WHERE id = ?
====> Parameters: 1(Integer)
<==== Columns: mentor_id, mentor_name, mentor_title
<==== Row: 1, 黄俪, 副教授
<== Row: 2, 白歌, 21, 计算机科学, 92.00, 1, 2
====> Preparing: SELECT id AS mentor_id, name AS mentor_name, title AS mentor_title FROM mentor WHERE id = ?
====> Parameters: 2(Integer)
<==== Columns: mentor_id, mentor_name, mentor_title
<==== Row: 2, 李眉, 教授
<== Row: 3, 小崔, 23, 信息安全, 85.00, 2, 1
====> Preparing: SELECT id AS mentor_id, name AS mentor_name, title AS mentor_title FROM mentor WHERE id = ?
====> Parameters: 1(Integer)
<==== Columns: mentor_id, mentor_name, mentor_title
<==== Row: 1, 黄俪, 副教授
<== Total: 3
分析:主查询 1 次,子查询 3 次(每行触发一次)。虽然黄俪被查询了两次,但由于是独立子查询,MyBatis 默认不会缓存跨语句的结果(除非开启二级缓存)。
方式二:嵌套结果映射(JOIN 查询)
查询结果集与方式一相同。
控制台 SQL 输出:
==> Preparing: SELECT s.id, s.name, s.age, s.major, s.score, s.class_id, s.mentor_id, m.id AS m_mentor_id, m.name AS m_mentor_name, m.title AS m_mentor_title FROM student s LEFT JOIN mentor m ON s.mentor_id = m.id
==> Parameters:
<== Columns: id, name, age, major, score, class_id, mentor_id, m_mentor_id, m_mentor_name, m_mentor_title
<== Row: 1, 大翔, 22, 软件工程, 89.50, 1, 1, 1, 黄俪, 副教授
<== Row: 2, 白歌, 21, 计算机科学, 92.00, 1, 2, 2, 李眉, 教授
<== Row: 3, 小崔, 23, 信息安全, 85.00, 2, 1, 1, 黄俪, 副教授
<== Total: 3
分析:仅执行 1 条 SQL,数据库往返一次完成。columnPrefix="m_" 让 MentorMap 内部只需声明 mentor_id,MyBatis 自动匹配 m_mentor_id 列。
分析
- 嵌套 Select 适合关联对象使用频率低、或需要延迟加载的场景。配置简单,但主查询返回 N 行就会触发 N 次子查询,数据量大时性能急剧下降(即 N+1 问题)。
- 嵌套结果映射 适合关联对象必用、或需要批量加载的场景。通过单次 JOIN 将数据一次性取回,MyBatis 在内存中按
student.id去重并组装对象,数据库压力最小。 fetchType="lazy"只能在嵌套 Select 方式下生效,因为嵌套结果映射已经在单次查询中返回了全部数据,不存在“后续加载”的时机。
易错场景 / 常见误区
| 误区 | 正解 |
|---|---|
嵌套 Select 方式下,column 写的是关联对象的列名 | column 应写主对象结果集中的外键列名,MyBatis 将该列值作为参数传给子查询 |
| 嵌套结果映射方式下,忘记给关联表列加别名前缀 | 多表 JOIN 时列名极易冲突,必须使用 columnPrefix 或显式别名,否则 MyBatis 可能映射到错误列 |
两种方式混用:同时写 select 和 resultMap | 二者互斥,只能选其一。同时存在时 MyBatis 行为未定义,可能抛出异常或忽略其中一个 |
认为 fetchType="lazy" 对嵌套结果映射也有效 | 延迟加载仅在嵌套 Select 方式下有意义;JOIN 查询已一次性取回数据,不存在延迟加载 |
子查询的 parameterType 与主查询 column 传递的列类型不匹配 | 确保 column 列的 JDBC 类型能被 MyBatis 正确转换为子查询参数类型,必要时使用 jdbcType 和 typeHandler |
面试考点
Q1:MyBatis 的 association 嵌套 Select 和嵌套结果映射有什么区别?分别适用于什么场景?
A:嵌套 Select 通过
column+select属性,在主查询之后对每行数据发起子查询;配置简单,支持延迟加载,但主查询 N 行会触发 N 次子查询,大数据量下产生 N+1 性能问题。嵌套结果映射通过单次 JOIN 查询,在内存中按<id>去重并组装对象;数据库往返仅一次,性能更优,但不支持延迟加载。适用场景:关联对象使用频率低或需要懒加载时选嵌套 Select;关联对象必用或批量查询时选嵌套结果映射。
Q2:为什么 association 的嵌套结果映射方式中,主对象的 <id> 配置至关重要?
A:嵌套结果映射使用单次 JOIN 查询,结果集中主对象列会重复出现(一对多 JOIN 时更明显)。MyBatis 依靠主对象的
<id>列值判断多行是否属于同一个 Java 实例。如果<id>缺失或配置错误,MyBatis 会将每一行都视为新对象,导致主对象重复、关联对象无法正确注入。
Q3:fetchType 的优先级是怎样的?如果全局开启了 lazyLoadingEnabled=true,但某 association 配置了 fetchType="eager",最终行为是什么?
A:
fetchType是局部配置,优先级高于全局lazyLoadingEnabled。因此该association会立即加载(eager),不受全局延迟加载开关影响。这种设计允许在全局懒加载策略下,对个别关键关联做例外处理。
小结
association 是 MyBatis 实现一对一关联映射的核心元素。嵌套 Select 方式以简单配置换取了潜在的 N+1 性能风险;嵌套结果映射方式以稍复杂的配置换取了单次数据库往返的高性能。理解两者的执行原理和适用边界,是设计高效持久层的关键。
下一章引子
一对一关联只是对象关系的基础形态。真实业务中更常见的是一对多关系:一个班级包含多名学生、一个订单包含多条明细。下一节将深入讲解 collection 元素,展示如何将 JOIN 结果集中的多行数据,正确映射为 Java 对象中的 List 集合。