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,覆盖了常见的类型转换,比如 String 到 VARCHAR、Integer 到 INT 之类的。但它的牛逼之处在于,你可以自定义 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,字段有 id、name、create_time、update_time。需求很简单:
- 插入新用户时,
create_time和update_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_time 和 update_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时,不管你传没传createTime和updateTime,它们都会被自动填成当前时间。 - 调用
updateUser时,update_time会被更新为当前时间。
接地气的优化:解决 SQL 类型判断问题
你可能注意到了,上面代码里 getSqlCommandType() 是个硬伤。TypeHandler 本身不知道当前 SQL 是 INSERT 还是 UPDATE,怎么办?别慌,咱们可以用点“接地气”的办法优化:
-
结合 Interceptor 或 ThreadLocal
在 SQL 执行前,用 Interceptor 拦截Executor或StatementHandler,把当前 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(); } } } -
偷懒版:直接在参数里传标志
如果不想搞 Interceptor,可以在实体类里加个字段sqlType,插入时传 "INSERT",更新时传 "UPDATE",TypeHandler 直接读这个字段判断。简单粗暴,但不优雅。
TypeHandler 还能干啥?互联网业务场景举例
除了填时间戳,TypeHandler 在互联网业务里还有不少用武之地:
-
枚举字段的自动转换
比如状态字段status,数据库存0、1、2,Java 用枚举Status {INACTIVE, ACTIVE, DELETED}。自定义个EnumTypeHandler,自动把数字转成枚举,省得每次手动映射。 -
JSON 字段的序列化/反序列化
互联网业务里,经常有字段存 JSON 串(比如extra_info)。写个JsonTypeHandler,插入时把对象转成 JSON 存数据库,查询时把 JSON 转回对象,爽歪歪。 -
加密字段的处理
用户手机号phone,存数据库前要加密,查出来时解密。弄个EncryptedStringTypeHandler,在setParameter里加密,getResult里解密,安全又方便。 -
批量操作的默认值填充
批量插入时,有些字段没传值(比如is_deleted默认0),TypeHandler 可以在setParameter里补默认值,省得前端一个个传。
总结:TypeHandler vs Interceptor
TypeHandler 虽然不像 Interceptor 那么万能,但它专注于字段级别的处理,简单直接,适合类型转换相关的需求。相比 Interceptor 的“全局拦截”,TypeHandler 更像是“精准打击”,配置灵活,用在公共字段自动填充上也够接地气。
在互联网业务里,尤其是数据量大、操作频繁的场景,自定义 TypeHandler 能让代码更简洁,维护更省心。试试看吧,也许它会成为你 MyBatis 工具箱里的新宠!