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;
}
}