MyBatis之TypeHandler用法

两个案例轻松理解 MyBatis 中的

在做开发时,我们经常会遇到这样一些问题,比如我有一个 Java 中的 Date 数据类型,我想将之存到数据库的时候存成一个 1970 年至今的毫秒数,怎么实现?再比如我有一个 User 类,User 类中有一个属性叫做 interest,这个属性用来描述用户的爱好,它的数据类型是一个 List 集合,那么我想在把这个 List 集合存入数据库的时候能够自动的变成{XXX,XXX,XXX}这样一个字符串然后存起来,当我从数据库读取的时候也是读取到这样一个字符串,读取成功之后再自动的将之转为一个 List 集合,OK,以上两种需求用我们传统的数据库读写操作肯定都是可以实现的,只不过工作量略大,在 mybatis 中有一个功能略强大的 typeHandler 专门用来解决数据库中的数据类型和 Java 中的数据类型之间的转化问题,那么我们今天以上面两种需求为例,来看看 typeHandler 要怎么使用。如果还有小伙伴对 mybatis 不太熟悉,建议先阅读一下前面几篇博客(初识 mybatis/初识 mybatis(二)/mybatis 常用配置/mybatis 映射器配置细则),本文的内容将在前面几篇博客的基础上展开,当然如果小伙伴有 mybatis 基础,那直接往下看即可。
事实上,mybatis 本身已经为我们提供了许多 typeHandler 了,系统提供的 typeHandler 能够满足我们日常开发中的大部分需求,如上这两种特殊的需求就需要我们自己去定义 typeHandler 了。

日期的转换

先来看日期的转换,假设我现在创建一张表,如下:
这里写图片描述
这张表中有一个字段叫做 regTime,这个字段表示用户的注册时间,它的数据类型为 varchar,OK,然后我再在 Java 中定义一个实体类:

public class User {
    private Long id;
    private String username;
    private String password;
    private Date regTime;
	// 省略 getter/setter
}

这个 JavaBean 中也有一个 regTime 字段,不同的是这里的数据类型是 Date,OK,如果我不做任何特殊处理,直接像初识 mybatis(二)这篇博客中介绍的那样向数据库插入数据,也是可以插入成功的,但是插入成功后是这样:
这里写图片描述
这个当然不是我想要的,我希望存到数据库里的是这样的:
这里写图片描述
就是我直接向数据库写数据,要写的是一个 Date 对象,但是写到数据库之后这个 Date 对象就变成了 Date 对象所描述的时间到 1970 年的秒数了,然后当我从数据库读取这个秒数之后,系统又会自动帮我将这个秒数转为 Date 对象,就是这样两个需求。这个时候,我们要做的事情其实很简单,那就是自定义 typeHandler,自定义 typeHandler 我们有两种方式,一种是实现 TypeHandler 接口,还有一种简化的写法就是继承自 BaseTypeHandler 类,我这里先以第二种为例来进行说明。

自定义 typeHandler 继承自 BaseTypeHandler

@MappedJdbcTypes({JdbcType.VARCHAR})
@MappedTypes({Date.class})
public class MyDateTypeHandler extends BaseTypeHandler<Date> {
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Date date, JdbcType jdbcType) throws SQLException {
        preparedStatement.setString(i, String.valueOf(date.getTime()));
    }
<span class="hljs-keyword">public</span> Date <span class="hljs-title function_">getNullableResult</span><span class="hljs-params">(ResultSet resultSet, String s)</span> <span class="hljs-keyword">throws</span> SQLException {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(resultSet.getLong(s));
}

<span class="hljs-keyword">public</span> Date <span class="hljs-title function_">getNullableResult</span><span class="hljs-params">(ResultSet resultSet, <span class="hljs-type">int</span> i)</span> <span class="hljs-keyword">throws</span> SQLException {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(resultSet.getLong(i));
}

<span class="hljs-keyword">public</span> Date <span class="hljs-title function_">getNullableResult</span><span class="hljs-params">(CallableStatement callableStatement, <span class="hljs-type">int</span> i)</span> <span class="hljs-keyword">throws</span> SQLException {
    <span class="hljs-keyword">return</span> callableStatement.getDate(i);
}

}

关于这个类我说如下几点:

1.@MappedJdbcTypes 定义的是 JdbcType 类型,这里的类型不可自己随意定义,必须要是枚举类 org.apache.ibatis.type.JdbcType 所枚举的数据类型。
2.@MappedTypes 定义的是 JavaType 的数据类型,描述了哪些 Java 类型可被拦截。
3. 在我们启用了我们自定义的这个 TypeHandler 之后,数据的读写都会被这个类所过滤
4. 在 setNonNullParameter 方法中,我们重新定义要写往数据库的数据。
5. 在另外三个方法中我们将从数据库读出的数据类型进行转换。

在 Mapper 中进行配置

自定义好了 typeHandler 之后,接下来我们需要在 userMapper.xml 中进行简单的配置,首先我们可以像上文说的,配置 resultMap,如下:

<resultMap id="userResultMap" type="org.sang.bean.User">
        <result typeHandler="org.sang.db.MyDateTypeHandler" column="regTime" javaType="java.util.Date"
                jdbcType="VARCHAR"
                property="regTime"/>
    </resultMap>

配置 resultMap 的时候我们指定了 javaType 和 jdbcType,同时也指定了处理的 typeHandler,然后在 select 中使用这个 resultMap:

<select id="getUser" resultMap="userResultMap">
        select * from user4
    </select>

但是这种方式有一个缺点那就是只适用于查询操作,即在查询的过程中系统会启用我们自定义的 typeHandler,会将秒数转为 Date 对象,但是在插入的时候却不会启用我们自定义的 typeHandler,想要在插入的时候启用自定义的 typeHandler,需要我们在 insert 节点中简单配置一下,如下:

<insert id="insertUser" parameterType="org.sang.bean.User">
        INSERT INTO user4(username,password,regTime) VALUES (#{username},#{password},#{regTime,javaType=Date,jdbcType=VARCHAR,typeHandler=org.sang.db.MyDateTypeHandler})
    </insert>

也可以只配置 javaType 和 jdbcType,如下:

<insert id="insertUser2">
        INSERT INTO user4(username,password,regTime) VALUES (#{username},#{password},#{regTime,javaType=Date,jdbcType=VARCHAR})
    </insert>

或者只配置 typeHandler:

<insert id="insertUser3">
        INSERT INTO user4(username,password,regTime) VALUES (#{username},#{password},#{regTime,typeHandler=org.sang.db.MyDateTypeHandler})
    </insert>

这三种效果都是一样的,都是在插入的时候将数据 Date 对象转为秒数。OK,如此之后,我们就可以实现将 Date 对象插入数据库之后变秒数以及将数据库中的秒数读取之后自动转为 Date 对象了。我们来看一个简单的测试:

    @Test
    public void test2() {
        SqlSession sqlSession = null;
        try {
            sqlSession = DBUtils.openSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = new User();
            user.setPassword("222222");
            user.setUsername("李四");
            Date regTime = new Date();
            user.setRegTime(regTime);
            userMapper.insertUser(user);
            sqlSession.commit();
        } catch (Exception e) {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
}

插入结果如下:
这里写图片描述

读取代码:

@Test
    public void test1() {
        SqlSession sqlSession = null;
        try {
            sqlSession = DBUtils.openSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> list = userMapper.getUser();
            for (User user : list) {
                System.out.println(user);
            }
            sqlSession.commit();
        } catch (Exception e) {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
}

读取结果如下:
这里写图片描述

OK,结果上面几步配置我们就完美的解决了读写时的数据转换问题了,读取时的数据转换除了我们上面介绍的定义 resultMap 然后在 select 节点中引用这种方式之外,也可以使用下面这种方式,注意下面这种方式只能解决读取时的数据转换问题

在配置文件中注册 typeHandler

我们需要在我们的 mybatis 配置文件中注册 typeHandler,注册有两种不同的方式,可以像下面这样一个类一个类的注册:

<typeHandlers>
        <typeHandler handler="org.sang.db.MyDateTypeHandler"/>
    </typeHandlers>

也可以直接注册一个包中所有的 typeHandler,系统在启动时会自动扫描包下的所有文件,如下:

<typeHandlers>
        <package name="org.sang.db"/>
    </typeHandlers>

这样配置完成之后,我们的目的就达到了,当我们进行数据库的读取操作的时候,秒数就会自动转为 Date 对象。

小结

OK,经过上面的介绍,想必小伙伴对 typeHandler 的使用已经有一定了解了,总结一下就是读取时的配置要和插入时的配置分贝来做,读取时数据转换我们有两种配置方式,分别是 resultMap 和在 mybatis 配置文件中配置 typeHandlers,插入时的配置就是在 insert 节点中进行配置。

List 集合的转换

OK,如果小伙伴们学会了如何把 Date 转为秒数,那么对于 List 集合的转换我就不再赘述了,道理都是一样的,大家可以可以直接在文末下载 Demo,Demo 中有 List 集合转换的案例。
这里给大家看两张效果图吧:
List 集合存入数据库之后变成这样:
这里写图片描述
读取出来之后又自动转为 List 集合了,下图是查询操作,实体类,和查询结果:
这里写图片描述

OK,以上就是我们对 typeHandler 的一个简单介绍。

本文案例下载:
本文案例 GitHub 地址 https://github.com/lenve/JavaEETest/tree/master/Test27-mybatis6。