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; public TypeHandlerRegistry () { this (new Configuration ()); } 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
中配置 JdbcType
和 JavaType
,或直接使用 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 >
在配置文件里面配置,结果集中字段指定的 JdbcType
和 JavaType
与定义的 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 方法的值,不是指枚举的一个属性),则可直接拿来使用。
所以从这里可知这两个枚举并不太适用 ,因为枚举通常会定义两个属性甚至多个属性。例如,code ,name ;入库保存 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;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 ) { ps.setString(i, parameter.name()); } else { ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); } } @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); SexEnum male = Enum.valueOf(SexEnum.class, "MALE" ); System.out.println(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;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" ); } 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); } }
输出结果:
自定义枚举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;@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。