玩转Mybatis中的类型转换器TypeHandler

1.场景

日常java开发中经常有这种需求,用0或者1这些代码(不局限于数字)来表示某种状态。比如用0表示女性,用1来表示男性。而且写入数据库可能是一个标识,从数据库读取又还原为具体的说明。而且一般情况下为了更好理解或者消除魔法值,通常的处理方案是定义一个枚举:

有些枚举是这样定义的

 public enum GenderType{
      FEMALE,MALE,UNKNOWN
 }

那么通常很多人会这么入库(java伪代码)

  if(GenderType.MALE){
   // 写入 1
  }else if(GenderType.FEMALE){
   // 写入 0
  }else{
  //也可能是泰国回来的 那就 2
  } 

读取的时候要么同样按照上面的再反向处理一次或者使用数据库sql语法case when 来直接写入DTO

 CASE gender
 WHEN 1 THEN '男'
 WHEN 0 THEN '女'
 ELSE '未知' END

这种处理方式看起来不是很优雅。而且多了很多的判断和处理逻辑,和我们的业务并不是非常相关。所以我们可以选择更好的处理方式。

2.Mybatis中的TypeHandler

如果你ORM框架用的是Mybatis。那么将很容易通过TypeHandler接口解决这个问题。

2.1 TypeHandler 分析

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;

}

源码分析:

  • setParameter 方法 通过 传入的T类型写你自己的逻辑,选择调用 PreparedStatement 对象的某个set方法将数据写入数据库。此方法用来写库。
  • getResult(ResultSet rs, String columnName) 通过字段名来读库并转换为T类型。
  • getResult(ResultSet rs, int columnIndex) 通过字段索引来读库并转换为T类型。
  • getResult(CallableStatement cs, int columnIndex) 调用存储过程来获取结果并转换为T类型。

2.2 EnumOrdinalTypeHandler

我们发现TypeHandler有一个实现类EnumOrdinalTypeHandler。字面意思是可以通过枚举的序号来处理类型。

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

我们先不考虑setNull的情况。通过此方法我们发现确实存入的是枚举的顺序值(顺序从0开始),拿上面的例子来说 如果是GenderType.FEMALE是0,如果是GenderType.MALE是1,但是当GenderType.UNKNOWN时存入的是2。取的时候也是自然反向处理为具体的GenderType枚举。

2.3 EnumTypeHandler

我们还发现有另外一个枚举类型处理器。它的set方法是这样的:

  @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); // see r3589
    }
  }

我们不考虑jdbcType问题发现都是将Enum.name()的值写入数据库。拿上面的例子来说 如果是GenderType.FEMALE是FEMALE,如果是GenderType.MALE是MALE,但是当GenderType.UNKNOWN时存入的是UNKNOWN。读库是通过Enum.valueOf(Class enumType,String name)来进行反转操作。

2.4 自定义TypeHandler

如果说我们的枚举类型或者说我们使用其他方式来处理类别转换怎么办?当然Mybatis不会帮你干这么具体的事情。需要你自己来实现了。我们还拿枚举作为例子,然后模仿上面的两种TypeHandler。
还是拿开始的例子来说通常我个人比较喜欢这么定义枚举:


public enum GenderTypeEnum {
    /**
     * female.
     */
    FEMALE(0, "女"),
     /**
     * male.
     */
    MALE(1,"男"),
    /**
     * unknown.
     */
    UNKNOWN(2, "未知");

    private int value;
    private String description;

    GenderType(int value, String description) {
        this.value = value;
        this.description = description;
    }
    
 
    public int value() {
        return this.value;
    }
    
 
    public String description() {
        return this.description;
    }
}

通过继承BaseTypeHandler实现该抽象类的3个钩子方法就行了:

@MappedTypes({GenderTypeEnum.class})
@MappedJdbcTypes({JdbcType.INTEGER})
public class GenderTypeEnumTypeHandler extends BaseTypeHandler<GenderTypeEnum> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, GenderTypeEnum parameter, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            ps.setInt(i, parameter.value());
        } else {
            // see r3589
            ps.setObject(i, parameter.value(), jdbcType.TYPE_CODE);
        }
    }

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

    @Override
    public GenderTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return getGenderType(rs.getInt(columnIndex));

    }

    @Override
    public GenderTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getGenderType(cs.getInt(columnIndex));
    }

    private GenderTypeEnum getGenderType(int value) {
        Class<GenderTypeEnum> genderTypeClass = GenderTypeEnum.class;
        return Arrays.stream(genderTypeClass.getEnumConstants())
                .filter(genderType -> genderType.value() == value)
                .findFirst().orElse(GenderTypeEnum.UNKNOWN);
    }
}

TypeHandler 实现写好了,那么如何让其发挥作用呢?我们接着往下走。

2.5 TypeHandler的核心要点

TypeHandler作用是javaType和jdbcType相互转换。所以在声明一个TypeHandler的时候一定要明确该TypeHandler处理的这两种类型。这是必须要明确的原则。MyBatis不会通过窥探数据库元信息来决定使用哪种JDBC类型,所以你必须在参数和结果映射中指明何种类型的字段,使其能够绑定到正确的类型处理器上。MyBatis直到语句被执行时才清楚数据类型。
通过上述例子中的@MappedJdbcTypes和@MappedTypes来进行绑定类型转换关系,也可以通过xml的typeHandler元素中的jdbcType或者javaType来指定。如果同时指定,xml的优先级要高。
注意有可能你会覆盖内置的TypeHandler。所以自定义时一定要去了解Mybatis提供的一些默认处理器。避免对其他业务的影响。所以使用自定义TypeHandler很重要的一个原则就是一定要声明JavaType和JdbcType.上面这些虽然比较生涩但是对于使用好TypeHandler非常重要。接下来我们来讲讲具体的配置。

2.6 免注册TypeHandler

我们这里只讲xml中的配置:

  • 一种在rultMap元素中声明一般用来查询。一定要注意2.5中的一些原则。
    <resultMap id="StudentMap" type="cn.felord.mybatis.entity.Student">
       <id column="student_id" property="studentId"/>
       <result column="student_name" property="studentName"/>
       <result column="gender" property="genderType" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
       <result column="age" property="age"/>
   </resultMap>
  • 然后是在插入、更新语句中使用。它们都是相同的,这里只举一个插入例子。
    <insert id="saveStu">
        insert into student (student_name, gender, age)
        values (#{studentName},
                #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER,typeHandler=cn.felord.mybatis.type.GenderTypeEnumTypeHandler},
                #{age})
    </insert>

如果注册了别名都可以使用别名。上面的好处就是不用在TypeHandlerRegistry中进行注册。

2.7 注册TypeHandler

在配置中声明注册TypeHandler,然后Mybatis根据两种类型会自动匹配。所以这里还是要强调2.5中的核心要点。

  • 如果你是xml配置需要在Configuration配置文件中的标签中进行声明式注册
<typeHandlers>
<typeHandler jdbcType="JdbcType枚举存在的枚举" javaType="typeAliases的别名或者全限定类名"  handler="类全限定名"/>
<package name="指定所有typeHandler所在的包的包名"/>
</typeHandlers>
  • javaConfig 方式 ,第一你可以通过SqlSessionFactory对象取到Configuration对象将typeHandler注册进去。如果你使用mybatis-spring组件,可以在SqlSessionFactoryBean
    的setTypeHandlersPackage方法中配置typeHandler的集中包路径,那么框架将会自动扫描并注册他们。springboot中对应的配置属性是mybatis.typeHandlersPackage。

如果你注册了TypeHandler。在Mapper.xml中只需要声明jdbcType和javaType,无需再声明具体的typeHandler。Mybatis会自动通过jdbcType、javaType来映射到具体注册的TypeHandler上去 。就像下面的例子

    <insert id="saveAutomaticStu">
        insert into student (student_name, gender, age)
        values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER}, #{age})
    </insert>

3.总结

今天我们学习了mybatis开发中如何通过使用类型处理器进行类型的转换处理,如何处理枚举,如何自定义处理器并使用它。相信对你在java开发过程中会有很大的帮助。相关的代码在我的码云仓库中:https://gitee.com/felord/mybatis-test.git

多多关注我的公众号 有更多干货奉上

![](https://ws1.sinaimg.cn/large/006Xmmmgly1g65c1rnjcuj30760763yz.jpg)

posted @ 2019-07-30 23:37  码农小胖哥  阅读(1977)  评论(0编辑  收藏  举报