MyBatis之TypeHandler用法

# 一。问题点

# 在使用 PostgreSQL 的过程中有用到数组类型与 json 类型,这些类型直接用 java 的类型去接收是会出现类型转换异常的,
怎么处理呢,这时候就要用到 TypeHandler 了

# 二。代码

  • java 数组类型接收 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
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;


/**
* json类型处理程序
*/
@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 类型,有如下途径 (优先级从上到下依次增加)。

      1. 类型处理器的泛型可以决定类型处理器支持的 JavaType
      1. 在类型处理器上使用注解 @MappedTypes 来指定,例如 @MappedTypes({List.class})
      1. 在配置文件中注册类型处理器时,通过 <typeHandler > 标签的 javaType 属性来指定,例如 <typeHandler handler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType="List"/>
  • # 如果要决定一个类型处理器支持哪些 Jdbc 类型,有如下途径 (优先级从上到下依次增加)。

      1. 在类型处理器上使用注解 @MappedJdbcTypes 来指定,例如 @MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
      1. 在配置文件中注册类型处理器时,通过 <typeHandler > 标签的 jdbcType 属性来指定(注意:同时也需要设置了 javaType
        属性,否则 jdbcType 属性不生效), 例如 <typeHandler handler="
        com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType=“List” jdbcType=“VARCHAR”/
        >。

# TypeHandler 如何生效

通常,TypeHandler 的使用场景有两个。

  1. 在设置预处理语句(PreparedStatement)中的参数时,完成 Java 类型到 Jdbc 类型的转换,通常就是 INSERT UPDATE
    的场景;
  2. 在将查询到的结果记录映射到 Java 实体对象时,完成 Jdbc 类型到 Java 类型的转换,通常就是会使用到 <resultMap
    > 的场景。

在使用场景下,如何让我们自定义的 TypeHandler 生效,如下直接给出结论,再做验证。

  1. 显式使用。示例中就是显示使用,即在 <result > 标签中和 #{} 占位符中使用 typeHandler
    属性来指定使用的类型处理器,这种方式是最简单粗暴的,就算不在配置文件中注册类型处理器,就算没有为类型处理器配置任何支持的
    Jdbc 类型,只要在 <result > 标签中和 #{} 占位符中使用了 typeHandler 属性来指定要使用的类型处理器,那么 **MyBatis
    ** 就会使用这个类型处理器;
  2. 隐式使用。通常,我们是不会关注到 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 做一个小结。

  1. 每一个 TypeHandler 都有其支持的 Java 类型,以及可能支持的 Jdbc 类型(也可能没有),并且在 MyBatis 中以 *
    Map<JavaType, Map<JdbcType, TypeHandler>>* 的形式存放;
  2. 如果有多个 TypeHandler 的支持的 Java 类型和 Jdbc 类型都一样,则后注册的 TypeHandler 会覆盖先注册的 *
    TypeHandler*;
  3. 如果在 MyBatis 的参数占位符 #{} 或者结果映射标签 <result > 中通过 javaType 属性指定了 JavaType,则 MyBatis
    在推断使用哪种 TypeHandler 时依据的 JavaType 会使用 javaType 属性的值,否则,如果是 <result > 的话则 MyBatis
    能根据映射对象推断出 JavaType,如果是 #{} 的话则 JavaType Object
  4. 如果在 MyBatis 的参数占位符 #{} 或者结果映射标签 <result > 中通过 jdbcType 属性指定了 JdbcType,则 MyBatis
    在推断使用哪种 TypeHandler 时依据的 JdbcType 会使用 jdbcType 属性的值,否则依据的 JdbcType 会为 null
  5. MyBatis 在推断使用哪个 TypeHandler 时,会先使用 JavaType 拿到 JavaType 对应的 Map<JdbcType,
    TypeHandler>
    ,然后使用 JdbcType 去匹配 TypeHandler,匹配不到则再使用 JdbcType=null 去匹配 TypeHandler
    ,如果还匹配不到,则判断 JavaType 对应的 TypeHandler 是否有多个,如果是多个则返回 null 表示匹配失败,如果只有一个则使用这个
    TypeHandler
  6. 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 {         // 根据需要设置到PreparedStatement中的参数判断出Java类型         // 然后再调用到TypeHandlerRegistry#getTypeHandler拿TypeHandler         handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);         if (handler == null || handler instanceof UnknownTypeHandler) {             handler = OBJECT_TYPE_HANDLER;         }     }     return handler; }

已知 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。自定义方式有两种,如下所示。

  1. 实现 TypeHandler 接口;
  2. 继承 BaseTypeHandler 抽象类。

更推荐使用继承 BaseTypeHandler 抽象类的方式来自定义 TypeHandler

自定义的 TypeHandler 有如下两种方式被使用。

  1. 显示使用。在 <result > 标签或者 #{} 中通过 typeHandler 属性指定要使用的 TypeHandler
  2. 隐式使用。通过 <result > 标签或者 #{} JavaType JdbcType,由 MyBatis 推断出需要使用的 TypeHandler