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

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

discriminator

导学

本节学习目标:

  • 理解 discriminator 解决的核心问题:单表数据映射到多态 Java 类型体系
  • 掌握 discriminator 的 column、javaType 属性以及 case 子元素的配置方式
  • 理解鉴别器在结果集解析阶段的执行时机和分支选择逻辑
  • 能够根据业务标识列设计合理的鉴别器映射策略
  • 明确鉴别器与 association / collection 的协作边界

定义

discriminator(鉴别器)是 MyBatis 中实现多态结果映射的元素。当数据库中的同一张表存储了多种类型的实体数据,且通过某个标识列(如 type、category、role)区分具体类型时,鉴别器允许 MyBatis 根据该列的值,在运行时选择不同的 resultMap 或 resultType,将同一行数据映射为不同的 Java 子类实例。

它解决的核心痛点:

  • 避免在业务层手动判断类型后再次转换对象
  • 将类型分支逻辑下沉到持久层,保持领域模型的多态性
  • 支持单表继承映射策略(Single Table Inheritance),减少数据库表数量

典型场景:学生表中的 student_type 列标识本科生(undergraduate)和研究生(postgraduate),二者在 Java 层分别对应 UndergraduateStudent 和 PostgraduateStudent 子类,子类拥有各自特有的属性(如本科生的 highSchool、研究生的 researchDirection)。

适用位置与核心属性

discriminator 只能嵌套在 <resultMap> 内部,且必须位于所有 <id>、<result>、<association>、<collection> 之后。其语法结构如下:

<resultMap id="BaseResultMap" type="com.flywing.entity.Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <!-- 其他公共属性映射 -->

    <discriminator javaType="string" column="student_type">
        <case value="undergraduate" resultType="com.flywing.entity.UndergraduateStudent"
             resultMap="UndergraduateMap">
            <result property="highSchool" column="high_school"/>
        </case>
        <case value="postgraduate" resultType="com.flywing.entity.PostgraduateStudent"
             resultMap="PostgraduateMap">
            <result property="researchDirection" column="research_direction"/>
            <result property="supervisorName" column="supervisor_name"/>
        </case>
    </discriminator>
</resultMap>
属性 / 子元素必填说明
column是鉴别所依据的数据库列名
javaType是该列对应的 Java 类型(用于类型转换和 case 的 value 比较)
jdbcType否JDBC 类型,辅助类型转换
typeHandler否自定义类型处理器
<case>至少一个分支定义,包含 value、resultType、resultMap 等属性
case.value是当 column 列值等于该值时,匹配此分支
case.resultType条件必填分支对应的 Java 类型(与 resultMap 二选一或同时存在)
case.resultMap条件必填分支引用的 resultMap ID(与 resultType 二选一或同时存在)

resultType 与 resultMap 的协作规则:

  • 若只指定 resultType:MyBatis 使用该类型创建对象,并继承父 resultMap 中鉴别器之前的所有映射,再加上 case 内部定义的映射。
  • 若只指定 resultMap:完全使用该 resultMap 进行映射,不再继承父 resultMap 中鉴别器之后的配置(但鉴别器之前的 <id> 和 <result> 仍会被继承)。
  • 若同时指定:以 resultMap 为准,resultType 仅作为创建对象的类型提示。

核心原理

鉴别器分支选择流程图

执行时机说明:

  1. 公共属性优先:MyBatis 先处理父 resultMap 中位于 <discriminator> 之前的 <id> 和 <result>,这些映射对所有子类通用。
  2. 分支判定:当解析到 <discriminator> 时,提取 column 列值,按顺序匹配 <case> 的 value。
  3. 对象创建与映射:匹配成功后,根据 case 的 resultType 或 resultMap 创建具体子类实例,并应用 case 内部定义的子类特有属性映射。
  4. 默认回退:如果没有 case 匹配,MyBatis 回退到父 resultMap 的 type 属性指定的类,继续完成剩余映射。

完整示例

场景说明

乐途公司学生管理系统中,student 表同时存储本科生和研究生。通过 student_type 列区分:

  • undergraduate:本科生,特有属性 highSchool(毕业高中)
  • postgraduate:研究生,特有属性 researchDirection(研究方向)、supervisorName(导师姓名)

我们将使用 discriminator 实现查询时自动映射到对应子类。

操作前的数据库表结构及初始数据

-- 学生表(扩展字段存储子类特有属性,可为NULL)
CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2),
    student_type VARCHAR(20),      -- 鉴别列: undergraduate / postgraduate
    high_school VARCHAR(50),       -- 本科生特有
    research_direction VARCHAR(50), -- 研究生特有
    supervisor_name VARCHAR(20),   -- 研究生特有
    class_id INT,
    mentor_id INT
);

初始数据:

idnameagemajorscorestudent_typehigh_schoolresearch_directionsupervisor_nameclass_idmentor_id
1大翔22软件工程89.50undergraduate乐途一中NULLNULL11
2白歌24人工智能91.00postgraduateNULL深度学习黄俪22
3小崔23信息安全85.00undergraduate乐途二中NULLNULL11

Java 实体类

package com.flywing.entity;

// 父类
public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;
    private String studentType;
    // getter / setter 省略...
}

// 本科生子类
public class UndergraduateStudent extends Student {
    private String highSchool;
    public String getHighSchool() { return highSchool; }
    public void setHighSchool(String highSchool) { this.highSchool = highSchool; }
}

// 研究生子类
public class PostgraduateStudent extends Student {
    private String researchDirection;
    private String supervisorName;
    public String getResearchDirection() { return researchDirection; }
    public void setResearchDirection(String researchDirection) { this.researchDirection = researchDirection; }
    public String getSupervisorName() { return supervisorName; }
    public void setSupervisorName(String supervisorName) { this.supervisorName = supervisorName; }
}

完整的映射文件片段

<?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,供case引用 -->
    <resultMap id="UndergraduateMap" type="com.flywing.entity.UndergraduateStudent">
        <result property="highSchool" column="high_school"/>
    </resultMap>

    <resultMap id="PostgraduateMap" type="com.flywing.entity.PostgraduateStudent">
        <result property="researchDirection" column="research_direction"/>
        <result property="supervisorName" column="supervisor_name"/>
    </resultMap>

    <!-- 父resultMap,包含discriminator -->
    <resultMap id="StudentPolymorphicMap" 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="studentType" column="student_type"/>

        <!-- 鉴别器:根据student_type选择子类 -->
        <discriminator javaType="string" column="student_type">
            <case value="undergraduate"
                  resultType="com.flywing.entity.UndergraduateStudent"
                  resultMap="UndergraduateMap"/>
            <case value="postgraduate"
                  resultType="com.flywing.entity.PostgraduateStudent"
                  resultMap="PostgraduateMap"/>
        </discriminator>
    </resultMap>

    <select id="selectAllStudents" resultMap="StudentPolymorphicMap">
        SELECT id, name, age, major, score, student_type,
               high_school, research_direction, supervisor_name
        FROM student
    </select>

</mapper>

实际执行结果

查询结果集(3 条记录映射为 2 种子类):

对象实际类型idnameagemajorscorestudent_type特有属性
UndergraduateStudent1大翔22软件工程89.50undergraduatehighSchool=乐途一中
PostgraduateStudent2白歌24人工智能91.00postgraduateresearchDirection=深度学习, supervisorName=黄俪
UndergraduateStudent3小崔23信息安全85.00undergraduatehighSchool=乐途二中

控制台 SQL 输出:

==>  Preparing: SELECT id, name, age, major, score, student_type, high_school, research_direction, supervisor_name FROM student
==>  Parameters:
<==  Columns: id, name, age, major, score, student_type, high_school, research_direction, supervisor_name
<==  Row: 1, 大翔, 22, 软件工程, 89.50, undergraduate, 乐途一中, NULL, NULL
<==  Row: 2, 白歌, 24, 人工智能, 91.00, postgraduate, NULL, 深度学习, 黄俪
<==  Row: 3, 小崔, 23, 信息安全, 85.00, undergraduate, 乐途二中, NULL, NULL
<==  Total: 3

Java 代码验证子类类型:

List<Student> students = mapper.selectAllStudents();
for (Student s : students) {
    if (s instanceof UndergraduateStudent) {
        UndergraduateStudent u = (UndergraduateStudent) s;
        System.out.println(u.getName() + " 是本科生,毕业于 " + u.getHighSchool());
    } else if (s instanceof PostgraduateStudent) {
        PostgraduateStudent p = (PostgraduateStudent) s;
        System.out.println(p.getName() + " 是研究生,研究方向 " + p.getResearchDirection());
    }
}

控制台输出:

大翔 是本科生,毕业于 乐途一中
白歌 是研究生,研究方向 深度学习
小崔 是本科生,毕业于 乐途二中

分析

  • 公共属性复用:id、name、age 等公共属性在父 resultMap 中定义一次,所有 case 自动继承,避免重复配置。
  • 子类扩展:case 内部或引用的 resultMap 中只声明子类特有属性,实现关注点分离。
  • 运行时多态:MyBatis 在结果集解析阶段完成类型判定和对象创建,调用方拿到的是已经分好类型的子类实例,无需二次判断。
  • NULL 列处理:本科生行的 research_direction 和 supervisor_name 为 NULL,由于 PostgraduateMap 未被匹配,这些列不会被映射到 UndergraduateStudent,不会引发空指针问题。

易错场景 / 常见误区

误区正解
将 discriminator 放在 <resultMap> 的最前面discriminator 必须位于所有 <id>、<result>、<association>、<collection> 之后,否则这些元素会被忽略或行为未定义
case 的 value 与数据库值大小写不一致case 的 value 匹配是字符串精确比较,需确保与数据库实际存储值的大小写完全一致
同时配置 resultType 和 resultMap 时,认为二者会合并同时存在时以 resultMap 为准,resultType 仅作为类型提示;resultMap 会覆盖 case 内部的内联映射
鉴别列值为 NULL 时,期望匹配某个 caseNULL 不会匹配任何 case,将回退到父 resultMap 的 type。如需处理,应在业务层或 SQL 中给鉴别列默认值
在 case 内部嵌套 association 或 collection语法上允许,但会使配置复杂化。建议将复杂映射抽离为独立 resultMap,通过 case.resultMap 引用

面试考点

Q1:discriminator 的 case 中,resultType 和 resultMap 有什么区别?如果同时配置了两者,以谁为准?

A:resultType 指定创建对象的 Java 类型,并继承父 resultMap 鉴别器之前的映射,再加上 case 内联的映射;resultMap 引用一个完整的映射定义,优先级更高。若同时配置,MyBatis 以 resultMap 为准进行映射,resultType 仅作为对象类型的补充提示。

Q2:discriminator 在 MyBatis 结果解析流程中的执行时机是什么?如果某行数据没有匹配任何 case,会发生什么?

A:discriminator 在父 resultMap 的公共属性(位于鉴别器之前的 <id>、<result>)映射完成后执行。它提取 column 列值,顺序匹配 case。如果没有 case 匹配,MyBatis 回退到父 resultMap 的 type 属性指定的类,继续完成剩余映射。这意味着调用方拿到的是父类实例,而非任何子类。

Q3:鉴别器模式与数据库设计中的“单表继承”有什么关系?它的优缺点是什么?

A:鉴别器映射正是单表继承(Single Table Inheritance)在持久层的实现。优点:查询简单,无需 JOIN 多张表;事务一致性易保证。缺点:表宽度过大,存在大量 NULL 列;子类属性无数据库级非空约束;鉴别列缺少枚举约束时容易存入非法值。适合子类数量少、差异属性不多的场景。

Q4:能否在 discriminator 的 case 中使用 association 或 collection?

A:可以。case 内部支持完整的 resultMap 子元素,包括 <id>、<result>、<association>、<collection>。但配置会变得冗长,推荐将复杂分支映射抽取为独立 resultMap,通过 case.resultMap 引用,保持父 resultMap 的简洁性。

小结

discriminator 为 MyBatis 提供了运行时多态映射能力,使单表继承策略在持久层得以优雅实现。通过 column 鉴别列和 case 分支定义,框架能在结果集解析阶段自动选择正确的 Java 子类,并将特有属性精准注入。合理使用 resultType 与 resultMap 的协作机制,可以在复用公共映射的同时,保持子类扩展的灵活性。

下一章引子

关联映射解决了对象结构的组装问题,但当关联数据量庞大且并非每次都需要时,一次性加载所有关联对象会造成资源浪费。下一节将深入探讨 延迟加载(Lazy Loading) 机制,揭示 MyBatis 如何通过 CGLIB 代理实现“按需查询”,以及相关的配置陷阱与调优策略。

上一页
collection
下一页
N+1查询问题