• 深入理解TypeHandler的用法
  • 发布于 2个月前
  • 220 热度
    0 评论
TypeHandler 即类型处理器,作用是将 Java 数据类型参数转成数据库的数据类型,或取出数据库数据转成 Java 数据类型。MyBatis 为 TypeHandler 提供了系统定义,也支持用户自定义,系统定义就可以实现大部分功能了。如果用户自定义 TypeHandler ,则需要小心谨慎。例如自定义 TypeHandler 实现枚举转换。

MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,或者从结果集(ResultSet)中取出一个值时,都会用注册了的 typeHandler。TypeHandler 常用的配置为 Java 类型(javaType),JDBC 类型(jdbcType)。

系统定义的typeHandler
在源码 org.apache.ibatis.type.TypeHandlerRegistry 类的构造方法中,可以看到默认注册的 typeHandler。
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;

  /**
   * 堆代码 duidaima.com
   * 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 的实现逻辑。
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个抽象方法,所以实现类需要实现这四个方法。
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 属性指定。配置如下:
<?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,就不用一个一个配置,减少配置工作量。
<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 源码:
package org.apache.ibatis.type;

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

/**
 * @堆代码 duidaima.com
 */
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()` 方法:
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 开始。
package org.apache.ibatis.type;

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

/**
 * @堆代码 duidaima.com
 */
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);
    }
  }
}
验证枚举元素的下标:
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);
    }
}
输出结果:
0
FEMALE

自定义枚举TypeHandler
大多数情况下,MyBatis默认的枚举类型处理类使用枚举名称或下标并不适用,则需要自定义枚举TypeHandler。
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;

/**
 * @堆代码 duidaima.com
 * @desc 自定义性别枚举处理器
 * @date 2023/7/30
 */
@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。
用户评论