MyBatis:typeHandler类型处理器及在映射中使用枚举

TypeHandler 即类型处理器,作用是将 Java 数据类型参数转成数据库的数据类型,或取出数据库数据转成 Java 数据类型。

MyBatis 为 TypeHandler 提供了系统定义,也支持用户自定义,系统定义就可以实现大部分功能了。如果用户自定义 TypeHandler ,则需要小心谨慎。例如自定义 TypeHandler 实现枚举转换。

MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,或者从结果集(ResultSet)中取出一个值时,都会用注册了的 typeHandler。

TypeHandler 常用的配置为 Java 类型(javaType),JDBC 类型(jdbcType)。

系统定义的typeHandler

在源码 org.apache.ibatis.type.TypeHandlerRegistry 类的构造方法中,可以看到默认注册的 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
public final class TypeHandlerRegistry {

private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
private final TypeHandler<Object> unknownTypeHandler;
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();

private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();

private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

/**
* The default constructor.
*/
public TypeHandlerRegistry() {
this(new Configuration());
}

/**
* The constructor that pass the MyBatis configuration.
*
* @param configuration a MyBatis configuration
* @since 3.5.4
*/
public TypeHandlerRegistry(Configuration configuration) {
this.unknownTypeHandler = new UnknownTypeHandler(configuration);

register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());

register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
//.........省略..........
}

以 StringTypeHandler 为例,了解 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
public class StringTypeHandler extends BaseTypeHandler<String> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}

@Override
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getString(columnName);
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return rs.getString(columnIndex);
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return cs.getString(columnIndex);
}
}

StringTypeHandler 是一个最常用的 typeHandler,处理 String 类型。

StringTypeHandler 继承了 BaseTypeHandler,而 BaseTypeHandler 实现了 TypeHandler 接口,TypeHandler 接口定义了4个抽象方法,所以实现类需要实现这四个方法。

1
2
3
4
5
6
7
8
9
10
11
public interface TypeHandler<T> {

void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

T getResult(ResultSet rs, String columnName) throws SQLException;

T getResult(ResultSet rs, int columnIndex) throws SQLException;

T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

BaseTypeHandler 实现了 setParameter 方法。

  • setParameter 是 PreparedStatement 对象设置参数,它允许我们自己自定义转换规则
  • getResult 是对 ResultSet 结果集的转换处理,分为用列名(columnName),或者使用列下标(columnIndex)来获取结果数据。还包括使用 CallableStatement(存储过程)获取结果及数据的方法。

自定义typeHandler

MyBatis 系统定义的 typeHandler 已经能够处理大部分的场景了了;而自定义 typeHandler 可以处理一些特殊的类型,如字典项的枚举。

自定义 typeHandler:必须实现接口 org.apache.ibatis.type.TypeHandler,也可继承 MyBatis 已经提供的 org.apache.ibatis.type.BaseTypeHandler 抽象类来实现,BaseTypeHandler 实现了 TypeHandler 接口。

自定义 typeHandler 类上使用注解来配置指定 JdbcType 和 JavaType。

  • @MappedTypes:定义的是 JavaType 类型,可以指定哪些 Java 类型被拦截。
  • @MappedJdbcTypes:定义的是 JdbcType 类型,它需要满足枚举类 org.apache.ibatis.type.JdbcType 所列的枚举类型。

MyBatis 默认情况下是不会启用自定义的 typeHandler 进行转换结果的,需要标识和指定,比如在字段映射的 ResultMap 中配置 JdbcTypeJavaType,或直接使用 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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.gxitsky.mapper.ActorMapper">

<resultMap id="actorMap" type="actor">
<id column="actor_id" property="actorId" javaType="long" jdbcType="BIGINT"/>
<result column="first_name" property="firstName" javaType="string" jdbcType="VARCHAR"/>
<result column="last_name" property="last_name" typeHandler="com.gxitsky.config.MyStringTypeHandler"/>
</resultMap>

<select id="queryById" parameterType="long" resultType="actor">
SELECT * FROM actor WHERE actor_id = #{actor_id}
</select>

<select id="queryById" resultMap="actorMap">
SELECT * FROM actor
</select>

<select id="findActor" resultMap="actorMap">
SELECT * FROM actor WHERE first_name LIKE concat('%',
#{firstName javaType=string jdbcType=VARCHAR typeHadler=com.gxitsky.config.MyStringTypeHandler} ,
'%');
</select>

</mapper>
  • 在配置文件里面配置,结果集中字段指定的 JdbcTypeJavaType 与定义的 typeHandler 一致,MyBatis 才能知道使用自定义的类型转换器进行转换。

    在配置 typeHandler 时也可以进行包配置,MyBatis 就会扫描包中的 typeHander,就不用一个一个配置,减少配置工作量。

    1
    2
    3
    4
    <typeHandlers>
    <typeHandler handler="com.gxitsky.config.mybatis.typehandler.MyStringTypeHandler" javaType="string" jdbcType="VARCHAR"/>
    <package name="com.gxitsky.config.mybatis.typehandler"/>
    </typeHandlers>
  • 映射集中的字段直接指定 typeHandler 属性,就不需要在配置文件中定义了。

  • 在参数中指定 typeHandler ,MyBatis 就会用对应的 typeHandler 进行转换,这样也不需要在配置里面定义。

枚举类型typeHandler

MyBatis 内部提供了两个转换枚举类型的 typeHandler:

  • org.apache.ibatis.type.EnumTypeHandler:使用枚举字符串名称作为参数传递。
  • org.apache.ibatis.type.EnumOrdinalTypeHandler:使用整数下标作为参数传递,MyBatis 默认的枚举类型处理器。

如果枚举和数据库字典项保持一致(例如,性别枚举,数据库字段保存男性的是 MALE, 枚举也是 MALE,指的是 Enum.name 方法的值,不是指枚举的一个属性),则可直接拿来使用。

所以从这里可知这两个枚举并不太适用,因为枚举通常会定义两个属性甚至多个属性。例如,codename;入库保存 code, 输出 name 用于显示。而不是简单的使用枚举元素的 name元素下标

EnumTypeHandler

EnumTypeHandler 是使用枚举名单处理 Java 枚举类型。EnumTypeHandler 对应的是一个字符串。

EnumTypeHandler 通过 Enum.name 方法将其转化为字符串,通过 Enum.valueOf 将字符串转化为枚举。

EnumTypeHandler 源码:

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
package org.apache.ibatis.type;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* @author Clinton Begin
*/
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

private final Class<E> type;

public EnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}

@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
// 取的是字典的 name 属性
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}

@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
// 返回枚举值
return s == null ? null : Enum.valueOf(type, s);
}

@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
// 返回枚举值
return s == null ? null : Enum.valueOf(type, s);
}

@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
// 返回枚举值
String s = cs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
}

验证枚举的 ``Enum.name()方法和Enum.valueOf()` 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum SexEnum {

MALE(1, "男"),
FEMAIL(2, "女"),
;

private int code;
private String name;

SexEnum(int code, String name) {
this.code = code;
this.name = name;
}

public static void main(String[] args) {
String name = SexEnum.MALE.name();
System.out.println(name);// name = MALE 字符串
SexEnum male = Enum.valueOf(SexEnum.class, "MALE");
System.out.println(male); // male = MALE 枚举值
}
}

EnumOrdinalTypeHandler

枚举类型是一个数组结构,枚举元素也是数组元素,是有下标的,下标依元素所在位置先后顺序,从 0 开始。

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
60
61
62
63
64
65
66
67
68
69
70
71
package org.apache.ibatis.type;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* @author Clinton Begin
*/
public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

private final Class<E> type;
private final E[] enums;

public EnumOrdinalTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
// 枚举class
this.type = type;
// 拿到所有元素
this.enums = type.getEnumConstants();
if (this.enums == null) {
throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
}

@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
// 入参取枚举的下标,存储的是枚举的下标
ps.setInt(i, parameter.ordinal());
}

@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
int ordinal = rs.getInt(columnName);
if (ordinal == 0 && rs.wasNull()) {
return null;
}
// 取出枚举的下标, 返回枚举
return toOrdinalEnum(ordinal);
}

@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int ordinal = rs.getInt(columnIndex);
if (ordinal == 0 && rs.wasNull()) {
return null;
}
return toOrdinalEnum(ordinal);
}

@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int ordinal = cs.getInt(columnIndex);
if (ordinal == 0 && cs.wasNull()) {
return null;
}
return toOrdinalEnum(ordinal);
}

private E toOrdinalEnum(int ordinal) {
try {
return enums[ordinal];
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert " + ordinal + " to " + type.getSimpleName() + " by ordinal value.", ex);
}
}
}

验证枚举元素的下标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum SexEnum {

FEMALE(2, "女"),
MALE(1, "男"),
;

private int code;
private String name;

SexEnum(int code, String name) {
this.code = code;
this.name = name;
}

public static void main(String[] args) {
int index = FEMALE.ordinal();
Class<SexEnum> sexEnumClass = SexEnum.class;
SexEnum[] enumConstants = sexEnumClass.getEnumConstants();
SexEnum enumConstant = enumConstants[index];
System.out.println(enumConstant);
}
}

输出结果:

1
2
0
FEMALE

自定义枚举TypeHandler

大多数情况下,MyBatis默认的枚举类型处理类使用枚举名称或下标并不适用,则需要自定义枚举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
40
41
42
43
44
45
package com.gxitsky.config;

import com.gxitsky.enums.SexEnum;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* @author gxing
* @desc 自定义性别枚举处理器
* @date 2023/7/16
*/
@MappedTypes(value = {Integer.class})
@MappedJdbcTypes(value = JdbcType.INTEGER)
public class SexEnumTypeHandler implements TypeHandler<SexEnum> {

@Override
public void setParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}

@Override
public SexEnum getResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return SexEnum.getByCode(code);
}

@Override
public SexEnum getResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return SexEnum.getByCode(code);
}

@Override
public SexEnum getResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return SexEnum.getByCode(code);
}
}

把映射结果集中的 sex 字段 typeHandler 改为 SexEnumTypeHandler。

MyBatis:typeHandler类型处理器及在映射中使用枚举

http://blog.gxitsky.com/2023/07/11/Mybatis-16-TypeHandler/

作者

光星

发布于

2023-07-11

更新于

2023-07-17

许可协议

评论