Mybatis 如何编写自定义TypeHandler,来实现特殊类型的处理?

MyBatis 默认提供的类型处理器无法满足你的需求时,例如:

  • 需要将 Java 中的枚举类型映射到数据库中的特定值(如 ID、代码),而不是默认的名称 (name()) 或序号 (ordinal())。
  • 需要处理 JSON 字符串与 Java 对象之间的转换。
  • 需要处理数据库中不支持的 Java 类型(如 Joda-Time、Java 8 Date/Time API 的某些特殊用法,虽然新版 MyBatis 对 Java 8 时间 API 支持较好)。
  • 需要对数据进行加密/解密处理。
  • 需要处理逗号分隔的字符串与 List<String> 之间的转换。

这时,我们就可以通过实现 org.apache.ibatis.type.TypeHandler 接口或继承其抽象基类 org.apache.ibatis.type.BaseTypeHandler 来创建自定义的类型处理器。通常推荐继承 BaseTypeHandler,因为它处理了 null 值检查,简化了实现。

编写自定义 TypeHandler 的步骤:

步骤 1:创建自定义 TypeHandler 类

  1. 创建一个 Java 类。

  2. 让这个类继承 org.apache.ibatis.type.BaseTypeHandler<YourJavaType>,其中 YourJavaType 是你这个处理器要处理的 Java 类型。

  3. 实现 BaseTypeHandler 中的四个抽象方法:

    • setNonNullParameter(PreparedStatement ps, int i, YourJavaType parameter, JdbcType jdbcType):
      • 作用: 将 Java 类型 (parameter) 转换为数据库能识别的类型,并设置到 PreparedStatement (ps) 的指定位置 (i)。
      • 参数:
        • ps: 当前的 PreparedStatement 对象。
        • i: 参数在 PreparedStatement 中的索引(从 1 开始)。
        • parameter: 从 Java 传入的非 null 参数值。
        • jdbcType: 目标 JDBC 类型(可能为 null,需要处理)。
    • getNullableResult(ResultSet rs, String columnName):
      • 作用:ResultSet (rs) 中根据列名 (columnName) 获取数据,并将其转换为目标 Java 类型 (YourJavaType)。
      • 返回值: 转换后的 Java 对象,如果数据库值为 null,应返回 null
    • getNullableResult(ResultSet rs, int columnIndex):
      • 作用:ResultSet (rs) 中根据列索引 (columnIndex) 获取数据,并将其转换为目标 Java 类型 (YourJavaType)。
      • 返回值: 转换后的 Java 对象,如果数据库值为 null,应返回 null
    • getNullableResult(CallableStatement cs, int columnIndex):
      • 作用: 从存储过程的 CallableStatement (cs) 中根据列索引 (columnIndex) 获取输出参数,并将其转换为目标 Java 类型 (YourJavaType)。
      • 返回值: 转换后的 Java 对象,如果数据库值为 null,应返回 null

示例:处理枚举类型映射到数据库中的整型 ID

假设有一个表示状态的枚举:

public enum Status {
    ACTIVE(1, "Active"),
    INACTIVE(0, "Inactive"),
    PENDING(2, "Pending");

    private final int id;
    private final String description;

    Status(int id, String description) {
        this.id = id;
        this.description = description;
    }

    public int getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    public static Status getById(Integer id) {
        if (id == null) {
            return null;
        }
        for (Status status : Status.values()) {
            if (status.id == id) {
                return status;
            }
        }
        throw new IllegalArgumentException("No Status found for id: " + id);
    }
}

现在编写一个 TypeHandler 来处理 Status 枚举和数据库中的 INT 类型之间的映射:

package com.example.typehandler;

import com.example.model.Status; // 引入你的 Status 枚举类
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 可选注解:声明此 Handler 处理的 Java 类型和 JDBC 类型
@MappedTypes(Status.class)       // 指定处理的 Java 类型
@MappedJdbcTypes(JdbcType.INTEGER) // 指定对应的 JDBC 类型
public class StatusTypeHandler extends BaseTypeHandler<Status> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Status parameter, JdbcType jdbcType) throws SQLException {
        // 将 Status 枚举的 ID 设置到 PreparedStatement
        ps.setInt(i, parameter.getId());
    }

    @Override
    public Status getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 从 ResultSet 根据列名获取 int 值
        int id = rs.getInt(columnName);
        // 如果 rs.wasNull() 为 true,表示数据库值为 NULL,BaseTypeHandler 会处理返回 null
        return rs.wasNull() ? null : Status.getById(id);
    }

    @Override
    public Status getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 从 ResultSet 根据列索引获取 int 值
        int id = rs.getInt(columnIndex);
        return rs.wasNull() ? null : Status.getById(id);
    }

    @Override
    public Status getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        // 从 CallableStatement 根据列索引获取 int 值
        int id = cs.getInt(columnIndex);
        return cs.wasNull() ? null : Status.getById(id);
    }
}

步骤 2:注册自定义 TypeHandler

有三种主要方式注册你的 TypeHandler,让 MyBatis 知道它的存在:

方式一:在 MyBatis 配置文件 (mybatis-config.xml) 中全局注册

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- ... 其他配置 ... -->

    <typeHandlers>
        <!-- 方式 1: 只指定 handler,MyBatis 会尝试通过泛型推断 Java 类型 -->
        <typeHandler handler="com.example.typehandler.StatusTypeHandler"/>

        <!-- 方式 2: 指定 handler 和 Java 类型,明确该 handler 处理哪个 Java 类型 -->
        <!-- <typeHandler javaType="com.example.model.Status" handler="com.example.typehandler.StatusTypeHandler"/> -->

        <!-- 方式 3: 指定 handler、Java 类型和 JDBC 类型,更精确的匹配 -->
        <!-- <typeHandler javaType="com.example.model.Status" jdbcType="INTEGER" handler="com.example.typehandler.StatusTypeHandler"/> -->

        <!-- 方式 4: 注册包,MyBatis 会自动扫描包下所有 TypeHandler (需要配合 @MappedTypes/@MappedJdbcTypes 注解) -->
        <!-- <package name="com.example.typehandler"/> -->
    </typeHandlers>

    <!-- ... 其他配置 ... -->
</configuration>
  • 推荐做法: 如果你的 TypeHandler 使用了 @MappedTypes 和/或 @MappedJdbcTypes 注解,可以使用 <package> 元素进行包扫描注册,或者只指定 handler 让 MyBatis 通过注解信息进行注册。如果没用注解,则明确指定 javaType 是个好习惯。

方式二:使用注解 @MappedTypes@MappedJdbcTypes (如上例所示)

TypeHandler 类上添加 @MappedTypes (指定处理的 Java 类型) 和 @MappedJdbcTypes (指定对应的 JDBC 类型) 注解。

  • 如果使用了包扫描注册 (<package name="..."/> 或 Spring Boot 集成中的 type-handlers-package 属性),MyBatis 会自动发现并注册带有这些注解的 TypeHandler

方式三:在 Mapper XML 文件中显式指定

可以在具体的参数映射 (parameterMap, #{}) 或结果映射 (resultMap, <result>) 中显式指定使用哪个 TypeHandler,这会覆盖全局注册的处理器。

<mapper namespace="com.example.mapper.UserMapper">

    <resultMap id="userResultMap" type="com.example.model.User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <!-- 显式指定 Status 字段使用 StatusTypeHandler -->
        <result property="status" column="status_id" typeHandler="com.example.typehandler.StatusTypeHandler"/>
    </resultMap>

    <select id="selectUserById" resultMap="userResultMap">
        SELECT id, username, status_id FROM users WHERE id = #{id}
    </select>

    <insert id="insertUser">
        INSERT INTO users (username, status_id)
        VALUES (
            #{username},
            <!-- 显式指定参数 status 使用 StatusTypeHandler -->
            #{status, typeHandler=com.example.typehandler.StatusTypeHandler, jdbcType=INTEGER}
        )
    </insert>

    <update id="updateUserStatus">
        UPDATE users SET
        status_id = #{status, typeHandler=com.example.typehandler.StatusTypeHandler, jdbcType=INTEGER}
        WHERE id = #{id}
    </update>

</mapper>
  • 注意: 在参数映射 #{} 中指定 typeHandler 时,通常也建议同时指定 jdbcType,特别是当参数可能为 null 时,这有助于 JDBC 驱动正确处理 null 值。

步骤 3:使用自定义 TypeHandler

一旦注册完成,MyBatis 在进行类型映射时:

  • 如果某个 Java 属性类型与全局注册或注解指定的 javaType 匹配,MyBatis 会自动使用对应的 TypeHandler
  • 如果在 Mapper XML 中显式指定了 typeHandler,则优先使用指定的 TypeHandler

之后,你就可以像处理普通类型一样在你的实体类中使用 Status 枚举,MyBatis 会通过你的 StatusTypeHandler 自动完成与数据库 INT 类型之间的转换。

总结:

编写自定义 TypeHandler 的核心是实现 TypeHandler 接口(或继承 BaseTypeHandler),处理好 Java 类型与 JDBC 类型之间的双向转换逻辑。然后通过配置文件、注解或在 Mapper XML 中显式指定的方式将其注册给 MyBatis,即可实现对特殊类型的无缝处理。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰糖心书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值