玩转mybatis中的类型转换

1.场景

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

有些枚举是这样定义的git

public enum GenderType{
      FEMALE,MALE,UNKNOWN
 }

那么一般不少人会这么入库(java伪代码)spring

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

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

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

这种处理方式看起来不是很优雅。并且多了不少的判断和处理逻辑,和咱们的业务并非很是相关。因此咱们能够选择更好的处理方式。数据库

2.Mybatis中的TypeHandler

若是你ORM框架用的是Mybatis。那么将很容易经过TypeHandler<T>接口解决这个问题。apache

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;

}

源码分析:springboot

  • 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。字面意思是能够经过枚举的序号来处理类型。mybatis

@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枚举。app

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<T> 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>标签中进行声明式注册
<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

多多关注个人公众号:码农小胖哥,能够获得更加及时的资讯和反馈。

相关文章
相关标签/搜索