MyBatis 除了 Interceptor 还能怎么实现公共字段自动填充?聊聊 TypeHandler 的妙用

363 阅读6分钟

MyBatis 除了 Interceptor 还能怎么实现公共字段自动填充?聊聊 TypeHandler 的妙用

在用 MyBatis 开发业务系统时,公共字段的自动填充是个绕不开的需求。比如,用户表的 create_time(创建时间)、update_time(更新时间)这些字段,几乎每次插入或更新数据时都得自动填上当前时间戳。要实现这个功能,大家可能第一时间想到的是 MyBatis 的 Interceptor(拦截器),毕竟它够灵活,能拦截 SQL 执行过程随便搞点“额外小动作”。但其实,除了 Interceptor,MyBatis 还有另一个开放接口——TypeHandler,也能干这事儿,而且在某些场景下用起来更直接、更接地气。今天就来聊聊 TypeHandler 是啥,怎么用它搞定公共字段自动填充,还能顺便解决哪些互联网业务需求。


TypeHandler 是啥?功能简介

简单来说,TypeHandler 是 MyBatis 提供的一个接口,用来处理 Java 类型和数据库 JDBC 类型之间的映射。每次你执行 SQL,比如用 PreparedStatement 设置参数,或者从 ResultSet 拿查询结果,都是 TypeHandler 在背后默默干活。MyBatis 自带了一堆内置的 TypeHandler,覆盖了常见的类型转换,比如 StringVARCHARIntegerINT 之类的。但它的牛逼之处在于,你可以自定义 TypeHandler,想怎么转换就怎么转换。

自定义 TypeHandler 需要实现这几个核心方法:

  • setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
    这个方法负责在执行 SQL 时,把 Java 对象的值塞到 PreparedStatement 里。比如你要插个时间字段,这里就能决定插什么值。

  • getResult(ResultSet rs, String columnName)
    从查询结果里按列名取值,转换成 Java 类型返回。比如数据库里是 TIMESTAMP,这里就能转成 java.sql.Timestamp

  • getResult(ResultSet rs, int columnIndex)
    和上面差不多,不过是用列索引取值。

  • getResult(CallableStatement cs, int columnIndex)
    这个是处理存储过程的返回值,一般用得少。

通过重写这些方法,你可以控制字段值的读写逻辑。听起来是不是有点像“字段级别的拦截器”?没错,TypeHandler 的本质就是让你在类型转换这个环节插手干点活儿。


用 TypeHandler 实现公共字段自动填充

好了,废话不多说,直接上例子。假设我们有个互联网业务场景:一个用户表 user,字段有 idnamecreate_timeupdate_time。需求很简单:

  • 插入新用户时,create_timeupdate_time 都自动填当前时间。
  • 更新用户信息时,update_time 自动更新为当前时间。

用 TypeHandler 怎么搞定?步骤如下:

1. 自定义一个 TimestampTypeHandler

我们写一个 TimestampTypeHandler,专门处理 java.sql.Timestamp 类型的时间字段。核心逻辑是根据 SQL 类型(INSERT 或 UPDATE)来决定填什么值。

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.sql.*;

public class TimestampTypeHandler implements TypeHandler<Timestamp> {

    @Override
    public void setParameter(PreparedStatement ps, int i, Timestamp parameter, JdbcType jdbcType) throws SQLException {
        // 获取当前 SQL 是 INSERT 还是 UPDATE(这里先假设有办法知道)
        String sqlCommandType = getSqlCommandType();
        if ("INSERT".equals(sqlCommandType)) {
            // 插入时,强制填当前时间
            ps.setTimestamp(i, new Timestamp(System.currentTimeMillis()));
        } else if ("UPDATE".equals(sqlCommandType)) {
            // 更新时,也填当前时间
            ps.setTimestamp(i, new Timestamp(System.currentTimeMillis()));
        } else {
            // 其他情况(比如 SELECT),用传入的值
            ps.setTimestamp(i, parameter);
        }
    }

    @Override
    public Timestamp getResult(ResultSet rs, String columnName) throws SQLException {
        return rs.getTimestamp(columnName);
    }

    @Override
    public Timestamp getResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getTimestamp(columnIndex);
    }

    @Override
    public Timestamp getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getTimestamp(columnIndex);
    }

    // 假设有个方法能拿到 SQL 命令类型(实际实现稍后说)
    private String getSqlCommandType() {
        return "INSERT"; // 先写死,后面优化
    }
}

2. 注册 TypeHandler

在 MyBatis 的配置文件里,把这个 TypeHandler 注册上,告诉 MyBatis 哪些字段用它来处理。

<typeHandlers>
    <typeHandler javaType="java.sql.Timestamp" handler="com.example.TimestampTypeHandler"/>
</typeHandlers>

3. Mapper XML 里用起来

在 Mapper 文件里,针对 create_timeupdate_time 字段,指定用我们的 TypeHandler。

<insert id="insertUser" parameterType="com.example.User">
    insert into user (id, name, create_time, update_time)
    values (
        #{id},
        #{name},
        #{createTime,jdbcType=TIMESTAMP,typeHandler=com.example.TimestampTypeHandler},
        #{updateTime,jdbcType=TIMESTAMP,typeHandler=com.example.TimestampTypeHandler}
    )
</insert>

<update id="updateUser" parameterType="com.example.User">
    update user 
    set name = #{name},
        update_time = #{updateTime,jdbcType=TIMESTAMP,typeHandler=com.example.TimestampTypeHandler}
    where id = #{id}
</update>

4. 实体类

实体类 User 保持简单,字段类型用 Timestamp

import java.sql.Timestamp;

public class User {
    private Long id;
    private String name;
    private Timestamp createTime;
    private Timestamp updateTime;

    // getters and setters
}

执行效果

  • 调用 insertUser 时,不管你传没传 createTimeupdateTime,它们都会被自动填成当前时间。
  • 调用 updateUser 时,update_time 会被更新为当前时间。

接地气的优化:解决 SQL 类型判断问题

你可能注意到了,上面代码里 getSqlCommandType() 是个硬伤。TypeHandler 本身不知道当前 SQL 是 INSERT 还是 UPDATE,怎么办?别慌,咱们可以用点“接地气”的办法优化:

  1. 结合 Interceptor 或 ThreadLocal
    在 SQL 执行前,用 Interceptor 拦截 ExecutorStatementHandler,把当前 SQL 类型(INSERT/UPDATE)存到 ThreadLocal 里。TypeHandler 再从 ThreadLocal 取出来判断。

    public class SqlCommandContext {
        private static final ThreadLocal<String> COMMAND_TYPE = new ThreadLocal<>();
    
        public static void setCommandType(String type) {
            COMMAND_TYPE.set(type);
        }
    
        public static String getCommandType() {
            return COMMAND_TYPE.get();
        }
    
        public static void clear() {
            COMMAND_TYPE.remove();
        }
    }
    
    // TypeHandler 修改
    private String getSqlCommandType() {
        return SqlCommandContext.getCommandType() != null ? SqlCommandContext.getCommandType() : "SELECT";
    }
    

    然后在 Interceptor 里设置:

    @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
    public class SqlCommandInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            SqlCommandContext.setCommandType(ms.getSqlCommandType().name());
            try {
                return invocation.proceed();
            } finally {
                SqlCommandContext.clear();
            }
        }
    }
    
  2. 偷懒版:直接在参数里传标志
    如果不想搞 Interceptor,可以在实体类里加个字段 sqlType,插入时传 "INSERT",更新时传 "UPDATE",TypeHandler 直接读这个字段判断。简单粗暴,但不优雅。


TypeHandler 还能干啥?互联网业务场景举例

除了填时间戳,TypeHandler 在互联网业务里还有不少用武之地:

  1. 枚举字段的自动转换
    比如状态字段 status,数据库存 012,Java 用枚举 Status {INACTIVE, ACTIVE, DELETED}。自定义个 EnumTypeHandler,自动把数字转成枚举,省得每次手动映射。

  2. JSON 字段的序列化/反序列化
    互联网业务里,经常有字段存 JSON 串(比如 extra_info)。写个 JsonTypeHandler,插入时把对象转成 JSON 存数据库,查询时把 JSON 转回对象,爽歪歪。

  3. 加密字段的处理
    用户手机号 phone,存数据库前要加密,查出来时解密。弄个 EncryptedStringTypeHandler,在 setParameter 里加密,getResult 里解密,安全又方便。

  4. 批量操作的默认值填充
    批量插入时,有些字段没传值(比如 is_deleted 默认 0),TypeHandler 可以在 setParameter 里补默认值,省得前端一个个传。


总结:TypeHandler vs Interceptor

TypeHandler 虽然不像 Interceptor 那么万能,但它专注于字段级别的处理,简单直接,适合类型转换相关的需求。相比 Interceptor 的“全局拦截”,TypeHandler 更像是“精准打击”,配置灵活,用在公共字段自动填充上也够接地气。

在互联网业务里,尤其是数据量大、操作频繁的场景,自定义 TypeHandler 能让代码更简洁,维护更省心。试试看吧,也许它会成为你 MyBatis 工具箱里的新宠!