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

    • 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执行器类型
    • 分页插件

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)
);

初始数据:

idnameagemajorscoreclass_idmentor_id
1大翔22软件工程89.5011
2白歌21计算机科学92.0012
3小崔23信息安全85.0021
idnametitle
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 条学生记录,每条带导师对象):

idnameagemajorscorementor.idmentor.namementor.title
1大翔22软件工程89.501黄俪副教授
2白歌21计算机科学92.002李眉教授
3小崔23信息安全85.001黄俪副教授

控制台 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 集合。

上一页
resultMap详解
下一页
collection