5.2 TypeHandler:类型缝合线


文档摘要

5.2 TypeHandler:类型缝合线 本节摘要:TypeHandler 负责单个 Java 类型与单个 JDBC 类型的双向转换,贯穿参数设值与结果取值两条通道。本节先看默认处理器的清单与枚举映射的两种内置选择,再手写一个"字符串列表与 JSON 列互转"的自定义处理器,走完实现、注册、验证的全程。 2.2 节参数装配时提到过它:值从 Java 到 JDBC 的类型翻译。这一节把这条隐形缝合线抽出来看清楚。 它站在哪两条通道上 参数入时把 Java 值翻译成 JDBC 类型设进语句;结果出时把 JDBC 值翻译回 Java 类型装进属性。

5.2 TypeHandler:类型缝合线

本节摘要:TypeHandler 负责单个 Java 类型与单个 JDBC 类型的双向转换,贯穿参数设值与结果取值两条通道。本节先看默认处理器的清单与枚举映射的两种内置选择,再手写一个"字符串列表与 JSON 列互转"的自定义处理器,走完实现、注册、验证的全程。

2.2 节参数装配时提到过它:值从 Java 到 JDBC 的类型翻译。这一节把这条隐形缝合线抽出来看清楚。

它站在哪两条通道上

参数入时把 Java 值翻译成 JDBC 类型设进语句;结果出时把 JDBC 值翻译回 Java 类型装进属性。同一个处理器服务两条通道:

它站在哪两条通道上

常见类型的默认翻译框架已经内置,日常无感:

Java 侧 JDBC 侧 默认行为
String VARCHAR / LONGVARCHAR 直通
Integer、Long 等 INTEGER / BIGINT 直通(null 安全)
BigDecimal DECIMAL 精度保留
Date、LocalDateTime TIMESTAMP 日期时间互转
Boolean BOOLEAN;或数值 0/1 按驱动方言
byte[] BLOB 二进制直通
Enum VARCHAR 或 INTEGER 两种内置处理器分野

枚举映射的两种内置选择

枚举是第一个"需要做选择"的场景。框架内置两种处理器,行为截然不同:

public enum OrderStatus { WAITING, PAID, CLOSED }
<!-- 写法一:默认按名字存 VARCHAR,列值是 PAID 这样的字符串 --> <resultMap id="orderMap" type="Order"> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/> </resultMap> <!-- 写法二:按序数存 INTEGER,列值是 0、1、2 --> <resultMap id="orderMap2" type="Order"> <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> </resultMap>

工程判断很明确:按名字存。序数绑定声明顺序——中间插一个新枚举值,历史数据的 1 就从 PAID 变成 CLOSED,这类事故在半夜最难查。除非表结构已定死为整型列,否则一律选名字存储。

手写一个:List 与 JSON 列互转

背景:商品表有个 tags 列存 JSON 字符串(如 '["秒杀","满减"]'),Java 实体想直接用 List tags。默认翻译器不认识这对组合,需要自定义处理器。

实现——继承 BaseTypeHandler,四个方法各管一条通道的一端:

// 自定义处理器:List<String> 与 JSON 文本互转 @MappedTypes(List.class) // 声明服务的 Java 类型 @MappedJdbcTypes(JdbcType.VARCHAR) // 声明服务的 JDBC 类型 public class StringListJsonHandler extends BaseTypeHandler<List<String>> { // 参数通道:Java 值 -> 设进语句 @Override public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, String.join("\",\"", parameter)); // 实际项目用 JSON 库序列化;此处手拼演示结构 } // 结果通道:按列名取值 -> Java 值 @Override public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } // 结果通道:按列下标取值 @Override public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } // 结果通道:存储过程出参取值 @Override public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private List<String> parse(String raw) { if (raw == null || raw.isBlank()) return new ArrayList<>(); // 去掉方括号与引号后按逗号拆分(示意实现,生产用 JSON 库) String body = raw.substring(1, raw.length() - 1); if (body.isEmpty()) return new ArrayList<>(); return Arrays.stream(body.split(",")) .map(s -> s.replace("\"", "").trim()) .collect(Collectors.toList()); } }

注册——三种方式按影响范围递增:

<!-- 方式一:全局注册,对整个应用的该类型组合生效 --> <typeHandlers> <typeHandler handler="com.example.handler.StringListJsonHandler"/> </typeHandlers>
<!-- 方式二:单个映射点声明,只在本地生效 --> <resultMap id="productMap" type="Product"> <result property="tags" column="tags" typeHandler="com.example.handler.StringListJsonHandler"/> </resultMap>
// 方式三:注解绑定在接口方法上 @Insert("INSERT INTO products(name, tags) VALUES(#{name}, #{tags, " + "typeHandler=com.example.handler.StringListJsonHandler})") int insert(Product product);

注意差异:参数侧的井号占位要显式指定 typeHandler(写在井号括号内),结果侧写在 resultMap 的 result 元素上——两边声明位置不同,漏了一边就是"存得进取不出"或反之。

验证

Product p = new Product(); p.setName("保温杯"); p.setTags(List.of("秒杀", "满减")); mapper.insert(p); session.commit(); Product db = mapper.findByName("保温杯"); // 输出:[秒杀, 满减] System.out.println(db.getTags());

⚠️ 常见坑:全局注册 List.class 会把所有 List 类型参数都导向这个处理器——实体里 List< Integer> 之类的字段会被误伤。窄化声明(@MappedJdbcTypes 限定 VARCHAR,或在 resultMap 里局部指定)比全局注册更安全;泛型擦除让注册表无法区分 List 的元素类型,这是 Java 侧的先天限制。

💡 关键直觉:TypeHandler 是"每个字段一对类型"的微型转换器,不是全局翻译服务。默认清单覆盖不到的组合(JSON、加密列、自定义值对象)才需要它;先确认确实没有现成处理器,再动手写。

本节要点回顾

  • 两条通道一个处理器:参数设值与结果取值共用同一实现,四个方法各管一端
  • 枚举按名字存:序数存储绑定声明顺序,插入新值会错位历史数据
  • 注册三方式:全局注册影响面最大,泛型类型慎用;映射点与注解声明是窄化手段
  • 两边都要声明:参数侧写在井号括号内,结果侧写在 resultMap,漏一边即单向故障
  • 先查默认清单再动手:大多数常见组合框架已内置

缝合线看完,下一节讲插队机制:插件如何在四大对象的执行链上插入自己的逻辑——分页、慢 SQL 审计都从这里生长。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U