Wenqi's Blog

MyBatis之TypeHandler解析

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的基类,里面有四个方法

1
2
3
4
5
6
7
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,并覆盖一下四个方法

1
2
3
4
5
6
7
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有如下属性

1
2
3
4
5
6
7
8
9
public class Student
{
private int id;
private String name;
private int age;
private SexEnum sex;
private String[] interests;
//getter and setter
}

其中interests属性是一个数组,我们第一步编写StringArrayTypeHandler,它继承自BaseTypeHandler,其前两个方法如下,它将String数组用逗号串联起来放入了PreparedStatement,又将从数据库中的值split后放入了String数组,从而实现了类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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接口

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

1
2
3
4
public interface StudentMapper {
int insertStudent(Student student);
Student findStudentById(int id);
}

配置Mapper文件

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

1
2
3
4
5
<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中

1
2
3
4
5
6
7
<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配置文件

1
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

关于源码

你可以在这里找到本文的全部源码,你同样可以在我的博客看到这篇文章。

坚持原创技术分享,您的支持将鼓励我继续创作!