# 一。问题点
# 在使用 PostgreSQL 的过程中有用到数组类型与 json 类型,这些类型直接用 java 的类型去接收是会出现类型转换异常的,
怎么处理呢,这时候就要用到 TypeHandler 了
# 二。代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.china315net.mybatis.handler;
import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.apache.ibatis.type.TypeException; import org.springframework.stereotype.Component;
import java.sql.Array; import java.sql.PreparedStatement; import java.sql.SQLException;
@Slf4j @MappedJdbcTypes({JdbcType.ARRAY}) @MappedTypes({String[].class, Short[].class, Integer[].class, Long[].class}) @Component public class ArrayTypeHandler extends org.apache.ibatis.type.ArrayTypeHandler {
@Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { log.debug("jdbcType:{}", jdbcType); if (parameter instanceof Array array) { ps.setArray(i, array); } else { if (!parameter.getClass().isArray()) { throw new TypeException("ArrayType Handler requires SQL array or java array parameter and does not support type " + parameter.getClass()); } Class<?> componentType = parameter.getClass().getComponentType(); String arrayTypeName = this.resolveTypeName(componentType); Array array = ps.getConnection().createArrayOf(arrayTypeName, (Object[]) parameter); ps.setArray(i, array); } } }
|
- java List 类型接收 jdbc array
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package com.china315net.mybatis.handler;
import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.springframework.stereotype.Component;
import java.sql.Array; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.List;
@Slf4j @MappedJdbcTypes({JdbcType.ARRAY}) @MappedTypes({List.class}) @Component public class ListToArrayTypeHandler extends org.apache.ibatis.type.ArrayTypeHandler {
@Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { if (parameter instanceof List<?> list) { if (!list.isEmpty()) { String arrayTypeName = this.resolveTypeName(list.get(0).getClass()); Array array = ps.getConnection().createArrayOf(arrayTypeName, list.toArray(Object[]::new)); ps.setArray(i, array); } } }
@Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { log.debug("getNullableResult1"); return this.extractList(rs.getArray(columnName)); }
@Override public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { log.debug("getNullableResult2"); return this.extractList(rs.getArray(columnIndex)); }
@Override public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { log.debug("getNullableResult3"); return this.extractList(cs.getArray(columnIndex)); }
private Object extractList(Array array) throws SQLException { return Arrays.stream(((Object[]) extractArray(array))).toList(); } }
|
- java 使用 FastJson2 的 JSONObject,JSONArray 类型接收 pgsql 的 json 或 jsonb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package com.china315net.mybatis.handler;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; import org.postgresql.util.PGobject; import org.springframework.stereotype.Component;
import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;
@MappedTypes({JSONObject.class, JSONArray.class}) @Component public class JsonbTypeHandler extends BaseTypeHandler<Object> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { PGobject jsonObject = new PGobject(); jsonObject.setType("json"); jsonObject.setValue(JSON.toJSONString(parameter)); ps.setObject(i, jsonObject); }
@Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { return JSON.parse(rs.getString(columnName)); }
@Override public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return JSON.parse(rs.getString(columnIndex)); }
@Override public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return JSON.parse(cs.getString(columnIndex)); } }
|
# 注意点:
# TypeHandler 接口方法说明
- setNonNullParameter 方法在写入的数据库操作时会调用
- getNullableResult 方法在查询的数据库操作时会调用
# 类型处理器支持的 Java 与 jdbc 类型
-
# 如果要决定一个类型处理器支持哪些 Java 类型,有如下途径 (优先级从上到下依次增加)。
-
- 类型处理器的泛型可以决定类型处理器支持的 JavaType;
-
- 在类型处理器上使用注解 @MappedTypes 来指定,例如 @MappedTypes({List.class});
-
- 在配置文件中注册类型处理器时,通过 <typeHandler > 标签的 javaType 属性来指定,例如
<typeHandler handler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType="List"/>
。
-
# 如果要决定一个类型处理器支持哪些 Jdbc 类型,有如下途径 (优先级从上到下依次增加)。
-
- 在类型处理器上使用注解 @MappedJdbcTypes 来指定,例如 @MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR});
-
- 在配置文件中注册类型处理器时,通过 <typeHandler > 标签的 jdbcType 属性来指定(注意:同时也需要设置了 javaType
属性,否则 jdbcType 属性不生效), 例如 <typeHandler handler="
com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType=“List” jdbcType=“VARCHAR”/>。
# TypeHandler 如何生效
通常,TypeHandler 的使用场景有两个。
- 在设置预处理语句(PreparedStatement)中的参数时,完成 Java 类型到 Jdbc 类型的转换,通常就是 INSERT 和 UPDATE
的场景;
- 在将查询到的结果记录映射到 Java 实体对象时,完成 Jdbc 类型到 Java 类型的转换,通常就是会使用到 <resultMap
> 的场景。
在使用场景下,如何让我们自定义的 TypeHandler 生效,如下直接给出结论,再做验证。
- 显式使用。示例中就是显示使用,即在 <result > 标签中和
#{}
占位符中使用 typeHandler
属性来指定使用的类型处理器,这种方式是最简单粗暴的,就算不在配置文件中注册类型处理器,就算没有为类型处理器配置任何支持的
Jdbc 类型,只要在 <result > 标签中和 #{}
占位符中使用了 typeHandler 属性来指定要使用的类型处理器,那么 **MyBatis
** 就会使用这个类型处理器;
- 隐式使用。通常,我们是不会关注到 TypeHandler 的,然而大部分时候 Java 类型到 Jdbc 类型的相互转换都能成功完成,是因为
MyBatis 会隐式使用其内置的 TypeHandler,而隐式使用哪个内置 TypeHandler,是通过 <result > 标签和 #{}
占位符的
JavaType 和 JdbcType 进行推断的。
显式使用没什么好说的,最为简单明了。下面重点说一下隐式使用。
首先能够被隐式使用的 TypeHandler,都需要完成注册,自定义的 TypeHandler 可以在配置文件中通过 <typeHandler
> 标签注册,而内置的 TypeHandler 是在 TypeHandlerRegistry#TypeHandlerRegistry 方法完成的注册,这个方法有点长,这里不再展示。
然后每一个 TypeHandler 都有其支持的 Java 类型,以及可能支持的 Jdbc 类型(也可能没有),TypeHandler 注册到 **MyBatis
** 中后,是按照如下形式存储的。
1
| Map<JavaType, Map<JdbcType, TypeHandler>>private final Map<Type, Map<JdbcType, TypeHandler<?>>>typeHandlerMap=new ConcurrentHashMap<>();
|
对隐式使用 TypeHandler 做一个小结。
- 每一个 TypeHandler 都有其支持的 Java 类型,以及可能支持的 Jdbc 类型(也可能没有),并且在 MyBatis 中以 *
Map<JavaType, Map<JdbcType, TypeHandler>>* 的形式存放;
- 如果有多个 TypeHandler 的支持的 Java 类型和 Jdbc 类型都一样,则后注册的 TypeHandler 会覆盖先注册的 *
TypeHandler*;
- 如果在 MyBatis 的参数占位符
#{}
或者结果映射标签 <result > 中通过 javaType 属性指定了 JavaType,则 MyBatis
在推断使用哪种 TypeHandler 时依据的 JavaType 会使用 javaType 属性的值,否则,如果是 <result > 的话则 MyBatis
能根据映射对象推断出 JavaType,如果是 #{}
的话则 JavaType 为 Object;
- 如果在 MyBatis 的参数占位符
#{}
或者结果映射标签 <result > 中通过 jdbcType 属性指定了 JdbcType,则 MyBatis
在推断使用哪种 TypeHandler 时依据的 JdbcType 会使用 jdbcType 属性的值,否则依据的 JdbcType 会为 null;
- MyBatis 在推断使用哪个 TypeHandler 时,会先使用 JavaType 拿到 JavaType 对应的 Map<JdbcType,
TypeHandler>,然后使用 JdbcType 去匹配 TypeHandler,匹配不到则再使用 JdbcType=null 去匹配 TypeHandler
,如果还匹配不到,则判断 JavaType 对应的 TypeHandler 是否有多个,如果是多个则返回 null 表示匹配失败,如果只有一个则使用这个
TypeHandler。
- spring 框架可以把 TypeHandler 实例注册到 IOC 容器,在使用时会根据类型去推断出要用哪个类型处理器
# 最后建议自定义的 TypeHandler 都要为其指定支持的 JavaType 和 JdbcType,以及必要时在 <result > 标签和 #{}
占位符中都把
javaType 和 jdbcType 属性配置上,这样 MyBatis 能够快速无误的帮我们推断出应该使用哪个类型处理器。
# 小补充
这里再对为参数占位符 #{}
推断类型处理器时的一些逻辑进行补充说明,不看也不影响对本篇文章的理解。
为参数占位符 #{}
推断类型处理器时,如果没有通过 javaType 来指定 Java 类型,那么 MyBatis 是无法知道 Java
类型是什么的(而 <result > 标签是可以的,这是不同点),此时 MyBatis 会默认 Java 类型是 Object,然后通过 Object 这个
JavaType 拿到一个 UnknownTypeHandler 内置类型处理器,下面看一下 UnknownTypeHandler 的 setNonNullParameter () 方法。
1
| @Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { TypeHandler handler = resolveTypeHandler(parameter, jdbcType); handler.setParameter(ps, i, parameter, jdbcType); } private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) { TypeHandler<?> handler; if (parameter == null) { handler = OBJECT_TYPE_HANDLER; } else {
|
已知 setNonNullParameter () 方法是会在实际执行 SQL 语句前被调用到,此时会完成 PreparedStatement 的参数设置,因此这时能够拿到实际设置到
PreparedStatement 中的参数值从而得到参数的 JavaType,所以这时会再尝试基于 JavaType 和 JdbcType 去匹配 *
TypeHandler*。
所以本质上就算没有通过 javaType 指定 JavaType,<result > 标签和 #{}
参数占位符都是能够拿到 JavaType,只不过 <*
result*> 标签在构建 ResultMapping 时就能够拿到 JavaType,而 #{}
参数占位符需要在 SQL 语句实际执行前为 *
PreparedStatement 设置参数时才能够拿到 JavaType*。
那么按照本节的结论,为什么第四节最后的例子 1 中的 #{}
使用不到 ListStringTypeHandler 呢,这是因为在为 *
PreparedStatement 设置参数时,studentIntention 这个参数的实际类型是 ArrayList*,而不是 List,但在 MyBatis 中,认为
ListStringTypeHandler 是支持 List 而不是 ArrayList 的。
# 总结
TypeHandler 能够帮助完成 Java 类型到 Jdbc 类型的相互转换,对于常规的转换,MyBatis 提供了内置的 TypeHandler
,而对于非常规的转换,需要自定义 TypeHandler。自定义方式有两种,如下所示。
- 实现 TypeHandler 接口;
- 继承 BaseTypeHandler 抽象类。
更推荐使用继承 BaseTypeHandler 抽象类的方式来自定义 TypeHandler。
自定义的 TypeHandler 有如下两种方式被使用。
- 显示使用。在 <result > 标签或者
#{}
中通过 typeHandler 属性指定要使用的 TypeHandler;
- 隐式使用。通过 <result > 标签或者
#{}
的 JavaType 和 JdbcType,由 MyBatis 推断出需要使用的 TypeHandler。