MyBatis作为一个ORM框架,在实现对象到关系数据库映射的过程中,一个无法避免的问题就是Java类型和JDBC类型之间的相互转换,而TypeHandler的作用就在于此,其作用是实现Java类型向JDBC类型之间的转换。

从上面的描述上来看,TypeHandler的作用对象是一个对象属性,从Java类型向JDBC类型之间的转换这样的说法或许过于抽象,举个例子来说,比如我们有个对象的属性是String数组,我想在插入数据库的时候将这个属性在插入数据库的时候是以一个varchar的属性记录,数组中每个元素以逗号相隔,这是从Java类型转换到JDBC;而从数据库获取这个对象的属性的时候又能够分割开逗号返回对象String数组,这是从JDBC转换到Java类型。
从实现层次上来说TypeHandler的功能,就是在我们使用MyBatis插入一个对象数据的时候,对象的每条属性是在经过如何处理后放入JDBC的SQL语句中,在取出一条数据库记录的时候,每一列的数据又是经过如何的处理放入对象的属性中的,这两个方面我们完全可以从MyBatis提供TypeHandler类中得以体现。

org.apache.ibatis.type.TypeHandler是所有TypeHandler的基类,里面有四个方法

void setParameter(PreparedStatement var1, int var2, T var3, JdbcType var4) throws SQLException;

    T getResult(ResultSet var1, String var2) throws SQLException;

    T getResult(ResultSet var1, int var2) throws SQLException;

    T getResult(CallableStatement var1, int var2) throws SQLException;

其中setParameter是将一个对象的属性经过转换后插入到PreparedStatement,后面三个方法是将从ResultSet中取出的值处理后返回给对象的属性(分别是通过列名,列索引来获取值,最后一个适用于存储过程)。而org.apache.ibatis.type.BaseTypeHandler则实现了TypeHandler,做了一些null值上的处理,一般情况下我们自定义的类型处理器直接继承自BaseTypeHandler,并覆盖一下四个方法

public abstract void setNonNullParameter(PreparedStatement var1, int var2, T var3, JdbcType var4) throws SQLException;

    public abstract T getNullableResult(ResultSet var1, String var2) throws SQLException;

    public abstract T getNullableResult(ResultSet var1, int var2) throws SQLException;

    public abstract T getNullableResult(CallableStatement var1, int var2) throws SQLException;

对于一下简单的TypeHandler MyBatis已经默认提供了,对于一些比较难处理的,比如数组,容器,自定义简单对象需要我们自己自定义TypeHandler进行类型处理。

自定义StringArrayTypeHandler


上图显示了在构建MyBatis程序的一般过程,其中橙色的线是我们在使用TypeHandler的过程中的一些重要步骤,我在图中做了数字标注。

编写StringArrayTypeHandler

首先第一步是编写属性的类型转换器,我们这里用编写的String数组的类型转换器作为例子。假设JavaBean是Student有如下属性

public class Student
{
    private int id;
    private String name;
    private int age;
    private SexEnum sex;
    private String[] interests;
    //getter and setter
}

其中interests属性是一个数组,我们第一步编写StringArrayTypeHandler,它继承自BaseTypeHandler

@Override
    /**
     * 将java类型转换为JDBC类型
     */
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, String[] strings, JdbcType jdbcType) throws SQLException {
        StringBuilder builder=new StringBuilder();
        for(String item: strings){
            builder.append(item+",");
        }
        builder.deleteCharAt(builder.length()-1);
        System.out.println(builder.toString());
        preparedStatement.setString(i,builder.toString());

    }

    @Override
    /**
     *通过列名获取Java类型的值
     */
    public String[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
        String result=resultSet.getString(s);
        if(result!=null){
            return result.split(",");
        }else{
            return null;
        }
    }

编写Mapper接口

第二步是先写业务需求的接口,我们这里定义了插入和查找两个接口

public interface StudentMapper {
    int insertStudent(Student student);
    Student findStudentById(int id);
}

配置Mapper文件

定义完业务接口就可以配置Mapper文件,这个是我们使用TypeHandler的地方,有两处使用到了我们定义的StringArrayTypeHandler,第一处是在我们插入值的参数处,使用了typeHandler参数。

<insert id="insertStudent" parameterType="TypeHandler.Student" useGeneratedKeys="true" keyProperty="id">
        insert into student(stu_name,stu_age,stu_interests,stu_sex) values(#{name},#{age},
        #{interests,typeHandler=TypeHandler.StringArrayTypeHandler},
        #{sex,typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler})
    </insert>

另一处是在select的resultMap中

<resultMap id="stuResult" type="TypeHandler.Student">
<id column="stu_id" property="id"/>
    <result column="stu_name" property="name"/>
    <result column="stu_age" property="age"/>
    <result column="stu_sex" property="sex" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    <result column="stu_interests" property="interests" typeHandler="TypeHandler.StringArrayTypeHandler"/>
</resultMap>

这两个方法Mapper方法的调用会引起我们自定义的StringArrayTypeHandler的setNonNullParameter和getNullableResult方法的调用。
Mapper配置文件和Mapper接口直接的管理是Mapper配置文件来维护的,Mapper配置文件namespace指定了Mapper接口的全称限名,并且对应的select,insert子标签的id对应于Mapper接口的方法名。

基础配置文件加载Mapper文件

第四步就是在MyBatis的基础配置文件中加载Mapper配置文件

<mapper resource="TypeHandler/StudentMapper.xml"/>

如果是非显示使用TypeHandler还要在基础配置文件中注册TypeHandler,显示使用则不用。显示使用是指直接指定TypeHandler的全称限名,非显示使用只指定type和jdbcType,让框架去寻找合适的TypeHandler

使用Mapper接口进行测试

这一阶段需要经过加载基础配置文件,获取SqlSessionFactory,打开SqlSession,getMapper执行sql语句的过程,至此整个过程结束,程序顺利运行

遇到的问题

  • 没有声明别名的类一定要写成全称限名
  • 主键回填不仅要设置useGeneratedKeys=true,还要设置keyProperty

EnumOrdinalTypeHandler和EnumTypeHandler

这两个都是MyBatis内置的处理Enum类型属性的类型转换器,其区别在于在存入数据库的时候EnumOrdinalTypeHandler是调用ordinal()方法获取索引存储,而EnumTypeHandler是调用name()方法获取名称进行存储,取回的时候也是同样策略的逆过程,比如我们上面使用了EnumOrdinalTypeHandler,其对应的枚举类型如下,而数据库存储的stu_sex为对应的索引0或1

public enum SexEnum {
    FEMALE("女"),MALE("男");
    private String sexName;
    SexEnum(String sexName){
        this.sexName=sexName;
    }

    public String getSexName() {
        return sexName;
    }

    public void setSexName(String sexName) {
        this.sexName = sexName;
    }
}