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仅作为创建对象的类型提示。
核心原理
鉴别器分支选择流程图
执行时机说明:
- 公共属性优先:MyBatis 先处理父
resultMap中位于<discriminator>之前的<id>和<result>,这些映射对所有子类通用。 - 分支判定:当解析到
<discriminator>时,提取column列值,按顺序匹配<case>的value。 - 对象创建与映射:匹配成功后,根据
case的resultType或resultMap创建具体子类实例,并应用case内部定义的子类特有属性映射。 - 默认回退:如果没有
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
);
初始数据:
| id | name | age | major | score | student_type | high_school | research_direction | supervisor_name | class_id | mentor_id |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 大翔 | 22 | 软件工程 | 89.50 | undergraduate | 乐途一中 | NULL | NULL | 1 | 1 |
| 2 | 白歌 | 24 | 人工智能 | 91.00 | postgraduate | NULL | 深度学习 | 黄俪 | 2 | 2 |
| 3 | 小崔 | 23 | 信息安全 | 85.00 | undergraduate | 乐途二中 | NULL | NULL | 1 | 1 |
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 种子类):
| 对象实际类型 | id | name | age | major | score | student_type | 特有属性 |
|---|---|---|---|---|---|---|---|
UndergraduateStudent | 1 | 大翔 | 22 | 软件工程 | 89.50 | undergraduate | highSchool=乐途一中 |
PostgraduateStudent | 2 | 白歌 | 24 | 人工智能 | 91.00 | postgraduate | researchDirection=深度学习, supervisorName=黄俪 |
UndergraduateStudent | 3 | 小崔 | 23 | 信息安全 | 85.00 | undergraduate | highSchool=乐途二中 |
控制台 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 时,期望匹配某个 case | NULL 不会匹配任何 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 代理实现“按需查询”,以及相关的配置陷阱与调优策略。