找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3102

积分

0

好友

424

主题
发表于 昨天 05:11 | 查看: 2| 回复: 0

参数:typeHandler 精准控制空值与类型转换

当数据库字段返回 NULL,而 MyBatis 无法自动推断 JDBC 类型(jdbcType)时,就会抛出异常。此时显式指定 typeHandler 可明确数据处理逻辑:

insert into t_role(id, role_name, note) values(#{id}, #{roleName, typeHandler=org.apache.ibatis.type.StringTypeHandler}, #{note})

✅ 实际开发中,MyBatis 多数场景能根据 javaTypejdbcType 自动匹配内置 typeHandler,无需手动声明;仅当字段类型未注册(如自定义枚举、JSON 字段等)时,才需显式配置。

若字段类型复杂,还可组合声明 Java 类型、JDBC 类型与自定义处理器:

#{age, javaType=int, jdbcType=NUMERIC, typeHandler=MyHandler}

数值精度也可精细控制,例如保留两位小数:

#{width, javaType=double, jdbcType=NUMERIC, numericScale=2}

存储过程参数:IN / OUT / INOUT 全支持

MyBatis 对存储过程提供原生支持,可清晰区分三类参数:

  • IN:由 Java 传入,数据库读取  
  • OUT:由数据库写入,Java 读取  
  • INOUT:双向传递  

对应 XML 写法如下:

#{id, mode=IN}
#{roleName, mode=OUT}
#{note, mode=INOUT}

对于简单输出参数(如 INTVARCHARDECIMAL),可直接通过 POJO 属性接收;若存储过程返回游标(CURSOR),MyBatis 同样支持 —— 这在分页查询、多结果集场景中尤为关键。

动态列名与动态表名:SQL 片段复用与运行时拼接

动态列名(基于 <sql> + <include>

适用于多表关联且列前缀不同的场景(如 Oracle 别名规范):

<!-- 定义 SQL 片段:支持别名动态注入 -->
<sql id="roleCols2">
    ${alias}.id, ${alias}.roleName, ${alias}.note
</sql>

<!-- 引用片段并传入别名 -->
<select id="getRoleById" parameterType="int" resultMap="roleMap" databaseId="oracle">
    SELECT 
        <include refid="roleCols2">
            <property name="alias" value="r"/>
        </include>
    FROM t_role r 
    WHERE id = #{id}
</select>

执行后生成的实际 SQL 为:

SELECT r.id, r.roleName, r.note
FROM t_role r 
WHERE id = ?

✅ 该方式提升 SQL 复用性,避免硬编码列名,增强可维护性。

动态表名(慎用!防 SQL 注入)

适用于分表、多租户等架构场景,但必须严格校验输入合法性

<!-- 定义通用查询片段 -->
<sql id="selectAllRecords">
    SELECT * FROM ${tableName}
</sql>

<!-- 接口方法:接收表名字符串 -->
<select id="queryAllByTableName" parameterType="java.lang.String" resultType="com.example.MyEntity">
    <include refid="selectAllRecords">
        <property name="tableName" value="#{tableName}"/>
    </include>
</select>

调用 queryAllByTableName("my_table") 时,最终执行:

SELECT * FROM my_table

⚠️ 安全提醒${} 不做预编译,务必对 tableName 做白名单校验(如正则 ^[a-zA-Z][a-zA-Z0-9_]{2,31}$),禁止用户直传。

resultMap:MyBatis 最强大的映射引擎

<resultMap> 是 MyBatis 的核心能力之一,支持复杂对象映射、类型转换、嵌套关联与条件鉴别。其子元素语义明确:

元素 用途 关键属性
<id> 主键映射 property, column, jdbcType, typeHandler
<result> 普通字段映射 同上,无主键语义
<association> 一对一关联(如员工 ↔ 工牌) property, javaType, column, fetchType, select
<collection> 一对多关联(如员工 ↔ 任务列表) property, ofType, column, fetchType, select
<discriminator> 类型鉴别器(如按性别加载不同体检表) column, javaType, <case> 分支
<constructor> 构造函数注入 <idArg>, <arg> 子元素

示例:构造函数 + 枚举类型处理器

<resultMap id="roleMap" type="com.ssm.pojo.Role">
    <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="roleName" javaType="java.lang.String"/>
        <arg column="note" javaType="com.ssm.pojo.SexEnum" typeHandler="com.ssm.Utils.SexEnumTypeHandler"/>
    </constructor>
    <id property="id" column="id"/>
    <result property="roleName" column="roleName"/>
    <result property="note" column="note"/>
</resultMap>

✅ 此配置让 MyBatis 调用 Role(int id, String roleName, SexEnum note) 构造器实例化对象,并自动将数据库 note 字段经 SexEnumTypeHandler 转换为枚举值。

注意事项:

  • 一旦使用 resultMap不可再同时声明 resultType
  • <resultMap> 标签内子元素顺序有约束:<id>/<result> 必须在 <association>/<collection> 之前,否则 IDE 报红(XML Schema 规范要求)。

查询结果存储:Map vs POJO,何时选谁?

MyBatis 支持直接以 Map 接收结果:

<select id="findColorByNote" parameterType="string" resultType="map">
    select id, color, note from t_color where note like concat('%', #{note}, '%')
</select>

✅ 优势:零 POJO 开发成本,适合临时查询、动态字段、快速原型。
❌ 劣势:丧失编译期检查、IDE 提示、序列化兼容性,可读性与可维护性差。

📌 推荐实践:  

  • 简单固定结构 → 用 POJO + resultType(自动映射);  
  • 需特殊转换(如枚举、日期格式、嵌套)→ 用 resultMap;  
  • 纯临时调试或元数据查询 → 才用 Map

级联:一对多、一对一与鉴别器(discriminator)实战

场景建模:员工体检按性别拆表

为适配男女体检项目差异,采用「单表继承」+ 「鉴别器」方案:

  • t_employee(员工主表)  
  • t_male_health_form(男性体检)  
  • t_female_health_form(女性体检)  
  • t_work_card(工牌)、t_employee_task(任务关联)、t_task(任务主表)

员工-体检-任务关系ER图

🔍 图中清晰展示外键依赖:t_male_health_form.employee_id → t_employee.idt_employee_task.employee_id → t_employee.id 等。建表时若未同步创建外键,可用 ALTER TABLE ... ADD CONSTRAINT 补充(详见后文 DDL)。

POJO 设计:继承体系支撑鉴别映射

// 基础健康表(男女共用字段)
public class HealthForm {
    private int id;
    private int employee_id;
    private String heart;
    private String lung;
    private String liver;
    private String kidney;
    private String spleen;
    private String note;
    // getter/setter...
}

// 女性扩展
public class FemaleHealthForm extends HealthForm {
    private String uterus; // 子宫特有
}

// 男性扩展
public class MaleHealthForm extends HealthForm {
    private String prostate; // 前列腺特有
}

// 员工主类(含关联属性)
public class Employee {
    private int id;
    private String real_name;
    private SexEnum sex;
    private Date birthday;
    private String mobile;
    private String email;
    private String position;
    private WordCard wordCard; // 一对一
    private List<EmployeeTask> employeeTaskList; // 一对多
}

XML 映射:<discriminator> 实现运行时类型路由

<resultMap id="employee" type="com.ssm.pojo.Employee">
    <id property="id" column="id"/>
    <result property="real_name" column="real_name"/>
    <result property="sex" column="sex" typeHandler="com.ssm.Utils.SexEnumTypeHandler"/>
    <result property="birthday" column="birthday"/>
    <result property="mobile" column="mobile"/>
    <result property="email" column="email"/>
    <result property="position" column="position"/>
    <result property="note" column="note"/>

    <!-- 关联工牌 -->
    <association property="wordCard" column="id" select="com.ssm.Dao.WorkCardDao.getWorkCardByEmployeeId"/>

    <!-- 关联任务列表 -->
    <collection property="employeeTaskList" column="id" select="com.ssm.Dao.EmployeeTaskDao.getEmployeeeTaskByEmployeeId"/>

    <!-- 鉴别器:根据 sex 字段决定加载哪个子类 -->
    <discriminator javaType="int" column="sex">
        <case value="1" resultMap="maleHealthFormMapper"/>
        <case value="2" resultMap="femaleHealthFormMapper"/>
    </discriminator>
</resultMap>

<!-- 男性员工完整映射 -->
<resultMap id="maleHealthFormMapper" type="com.ssm.pojo.MaleEmployee" extends="employee">
    <association property="maleHealthForm" column="id" select="com.ssm.Dao.MaleHealthFormDao.getMaleHealthFormById"/>
</resultMap>

<!-- 女性员工完整映射 -->
<resultMap id="femaleHealthFormMapper" type="com.ssm.pojo.FemaleEmployee" extends="employee">
    <association property="femaleHealthForm" column="id" select="com.ssm.Dao.FemaleHealthFormDao.getFemaleHealthFormById"/>
</resultMap>

⚠️ 性能提示:级联深度建议 ≤ 3 层;超过则考虑拆分为多次查询 + 应用层组装。MyBatis 不支持原生多对多级联,但可通过「两次一对多」模拟(如 Employee → Task + Task → Employee)。

延迟加载(Lazy Loading):按需加载,降低 N+1 查询风险

MyBatis 提供两级延迟控制:

配置项 说明 默认值(3.4.1+)
lazyLoadingEnabled 是否启用延迟加载 false
aggressiveLazyLoading 是否“激进”:任意属性访问即加载全部延迟对象 false

开启后,employee.getWordCard() 第一次调用才触发 WorkCardDao.getWorkCardByEmployeeId 查询;后续调用直接返回缓存对象。

✅ 推荐在 mybatis-config.xml 中全局开启:

<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

缓存机制:一级缓存(SqlSession 级)与二级缓存(Mapper 命名空间级)

一级缓存(默认开启,无需配置)

作用域:单个 SqlSession 生命周期内。
行为:同一 SqlSession 中,相同 SQL + 相同参数 → 第二次查询直接命中缓存,不发 SQL

SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession();
EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);

Employee employee1 = employeeDao.getEmployeeById(1); // 执行 SQL
Employee employee2 = employeeDao.getEmployeeById(1); // 直接返回缓存

✅ 无需 POJO 实现 Serializable
❌ 不同 SqlSession 间不共享。

二级缓存(需显式开启)

作用域:整个 Mapper 命名空间(如 com.ssm.Dao.EmployeeDao)。
前提:POJO 必须实现 java.io.Serializable(因需序列化跨 Session 传输):

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L; // 显式声明
    // ...
}

MyBatis二级缓存配置示意图

启用步骤:

  1. 全局开关mybatis-config.xml):

    <settings>
       <setting name="cacheEnabled" value="true"/>
    </settings>
  2. Mapper 文件启用(如 EmployeeDao.xml):

    <mapper namespace="com.ssm.Dao.EmployeeDao">
       <cache/> <!-- 启用默认二级缓存 -->
       <!-- 或自定义配置 -->
       <cache
           eviction="LRU"
           flushInterval="60000"
           size="512"
           readOnly="true"
           type="com.example.RedisCache">
           <property name="redisHost" value="localhost"/>
           <property name="redisPort" value="6379"/>
       </cache>
    </mapper>
  3. SQL 级控制(可选):

    <select id="selectById" useCache="true">...</select>
    <insert flushCache="true">...</insert> <!-- 默认 true,自动清缓存 -->

缓存参数详解:

属性 可选值 说明
eviction LRU, FIFO, SOFT, WEAK 缓存淘汰策略,默认 LRU
flushInterval 毫秒数(如 60000 定时刷新间隔,0 表示不刷新
size 正整数(如 512 最大缓存条目数
readOnly true/false true:返回只读对象(性能优);false:返回可修改副本(线程安全)
type 自定义 Cache 实现类全限定名 如集成 Redis、Caffeine 等

🔒 重要提醒:二级缓存在分布式环境下不保证强一致性。高并发更新场景下,建议结合业务容忍度评估是否启用;若需跨节点同步,应选用 Redis 等外部缓存并配置 type

存储过程进阶:IN/OUT 参数与游标(CURSOR)处理

IN/OUT 参数存储过程调用

以统计角色数量为例(Oracle):

CREATE OR REPLACE PROCEDURE count_role(
    p_role_name IN VARCHAR,
    count_total OUT INT,
    exec_date OUT DATE
) IS
BEGIN
    SELECT COUNT(*) INTO count_total FROM t_role WHERE role_name LIKE '%' || p_role_name || '%';
    SELECT SYSDATE INTO exec_date FROM DUAL;
END;

对应 Java POJO 封装参数:

public class PdCountRoleParams {
    private String roleName;
    private int total;
    private Date execDate;
    // getter/setter...
}

Mapper XML 调用:

<select id="countRole" statementType="CALLABLE" parameterType="com.ssm.pojo.param.PdCountRoleParams">
    {call count_role(
        #{roleName, mode=IN, jdbcType=VARCHAR},
        #{total, mode=OUT, jdbcType=INTEGER},
        #{execDate, mode=OUT, jdbcType=DATE}
    )}
</select>

游标(CURSOR)返回多行结果

存储过程返回 SYS_REFCURSOR,MyBatis 通过 jdbcType=CURSOR + resultMap 解析:

CREATE OR REPLACE PROCEDURE find_role(
    p_role_name IN VARCHAR,
    p_start IN INT,
    p_end IN INT,
    r_count OUT INT,
    ref_cur OUT SYS_REFCURSOR
) AS
BEGIN
    SELECT COUNT(*) INTO r_count FROM t_role WHERE role_name LIKE '%' || p_role_name || '%';
    OPEN ref_cur FOR
        SELECT id, role_name, note FROM (
            SELECT id, role_name, note, ROWNUM as row1 
            FROM t_role a 
            WHERE a.role_name LIKE '%' || p_role_name || '%' AND ROWNUM <= p_end
        ) WHERE row1 > p_start;
END;

Mapper XML:

<resultMap type="role" id="roleMap2">
    <id property="id" column="id"/>
    <result property="roleName" column="role_name"/>
    <result property="note" column="note"/>
</resultMap>

<select id="findRole" statementType="CALLABLE" parameterType="com.ssm.pojo.param.PdFindRoleParams">
    {call find_role(
        #{roleName, mode=IN, jdbcType=VARCHAR},
        #{start, mode=IN, jdbcType=INTEGER},
        #{end, mode=IN, jdbcType=INTEGER},
        #{total, mode=OUT, jdbcType=INTEGER},
        #{roleList, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=roleMap2}
    )}
</select>

roleList 字段将被自动填充为 List<Role>total 字段接收计数结果。

数据库建表脚本(含外键约束)

以下 SQL 按依赖顺序执行,确保外键引用有效:

-- 删除旧表(按依赖逆序)
DROP TABLE IF EXISTS t_female_health_form;
DROP TABLE IF EXISTS t_male_health_form;
DROP TABLE IF EXISTS t_task;
DROP TABLE IF EXISTS t_work_card;
DROP TABLE IF EXISTS t_employee;
DROP TABLE IF EXISTS t_employee_task;

-- 创建员工主表
CREATE TABLE t_employee (
    id int(12) NOT NULL AUTO_INCREMENT,
    real_name varchar(60) NOT NULL,
    sex int(2) NOT NULL COMMENT '1:男 2:女',
    birthday date NOT NULL,
    mobile varchar(20) NOT NULL,
    email varchar(60) NOT NULL,
    position varchar(60) NOT NULL,
    note varchar(256) DEFAULT NULL,
    PRIMARY KEY (id)
);

-- 创建员工任务关联表
CREATE TABLE t_employee_task (
    id int(12) NOT NULL,
    employee_id int(12) NOT NULL,
    task_id int(12) NOT NULL,
    task_name VARCHAR(60) NOT NULL,
    note varchar(256) DEFAULT NULL,
    PRIMARY KEY (id),
    KEY FK_employee_id (employee_id),
    CONSTRAINT FK_employee_id FOREIGN KEY (employee_id) REFERENCES t_employee (id),
    KEY FK_task_id (task_id),
    CONSTRAINT FK_task_id FOREIGN KEY (task_id) REFERENCES t_task (id)
);

-- 创建男性健康表
CREATE TABLE t_male_health_form (
    id int(12) NOT NULL AUTO_INCREMENT,
    employee_id int(12) DEFAULT NULL,
    heart varchar(64) NOT NULL,
    liver varchar(64) NOT NULL,
    spleen varchar(64) NOT NULL,
    lung varchar(64) NOT NULL,
    kidney varchar(64) NOT NULL,
    prostate varchar(64) NOT NULL,
    note varchar(256) NOT NULL,
    PRIMARY KEY (id),
    KEY FK_employee_id_1 (employee_id),
    CONSTRAINT FK_employee_id_1 FOREIGN KEY (employee_id) REFERENCES t_employee (id)
);

-- 创建女性健康表
CREATE TABLE t_female_health_form (
    id int(12) NOT NULL AUTO_INCREMENT,
    employee_id int(12) NOT NULL,
    heart varchar(64) NOT NULL,
    liver varchar(64) NOT NULL,
    spleen varchar(64) NOT NULL,
    lung varchar(64) NOT NULL,
    kidney varchar(64) NOT NULL,
    uterus varchar(64) NOT NULL,
    note varchar(255) NOT NULL,
    PRIMARY KEY (id),
    KEY FK_employee_id_2 (employee_id),
    CONSTRAINT FK_employee_id_2 FOREIGN KEY (employee_id) REFERENCES t_employee (id)
);

-- 创建工作卡表
CREATE TABLE t_work_card (
    id int(12) NOT NULL AUTO_INCREMENT,
    employee_id int(12) NOT NULL,
    real_name varchar(60) NOT NULL,
    department varchar(20) NOT NULL,
    mobile varchar(20) NOT NULL,
    position varchar(20) NOT NULL,
    note varchar(256) DEFAULT NULL,
    PRIMARY KEY (id),
    KEY FK_employee_id (employee_id),
    CONSTRAINT FK_employee_id FOREIGN KEY (employee_id) REFERENCES t_employee (id)
);

-- 创建任务表
CREATE TABLE t_task (
    id int(12) NOT NULL,
    title varchar(256) NOT NULL,
    context varchar(256) NOT NULL,
    note varchar(256) DEFAULT NULL,
    PRIMARY KEY (id)
);

补充外键(若建表时未包含)

-- 为已存在表添加外键约束
ALTER TABLE t_employee_task ADD CONSTRAINT FK_REFERENCE_1 FOREIGN KEY (employee_id) REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_employee_task ADD CONSTRAINT FK_REFERENCE_2 FOREIGN KEY (task_id) REFERENCES t_task (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_female_health_form ADD CONSTRAINT FK_REFERENCE_3 FOREIGN KEY (employee_id) REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_male_health_form ADD CONSTRAINT FK_REFERENCE_4 FOREIGN KEY (employee_id) REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_work_card ADD CONSTRAINT FK_REFERENCE_5 FOREIGN KEY (employee_id) REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;

总结:从 CRUD 到工程化落地的关键跃迁

掌握 MyBatis 映射器高级特性,是脱离模板化开发、迈向高可用架构的必经之路:

  • typeHandler 是类型安全的基石,尤其在微服务间 JSON 交互、枚举标准化场景中不可或缺;  
  • 动态 SQL(${} / #{})赋予 SQL 构建灵活性,但务必严守注入防线;  
  • <resultMap> + <discriminator> 让复杂领域模型与数据库物理设计解耦,支撑业务演进;  
  • 二级缓存显著降低 DB 压力,但需权衡一致性与复杂度,云栈社区Java 板块提供了大量生产级缓存治理案例;  
  • 存储过程支持让 MyBatis 在遗留系统集成、高性能计算场景中依然保持竞争力。

💡 若你正在构建企业级 Java 应用,建议将本文实践与 后端 & 架构 板块中的分布式事务、读写分离方案结合,形成完整的数据访问层技术栈。




上一篇:MyBatis动态SQL详解:告别手写冗余SQL,高效构建条件查询与更新
下一篇:Java反射机制深度解析:核心用法与Class类操作指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 09:00 , Processed in 0.679079 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表