mybatis中typeHandler的两个案例

在做开发时,我们经常会遇到这样一些问题,

比如我有一个Java中的Date数据类型,我想将之存到数据库的时候存成一个1970年至今的毫秒数,怎么实现?

再比如我有一个User类,User类中有一个属性叫做interest,这个属性用来描述用户的爱好,它的数据类型是一个List集合,那么我想在把这个List集合存入数据库的时候能够自动的变成{XXX,XXX,XXX}这样一个字符串然后存起来,当我从数据库读取的时候也是读取到这样一个字符串,读取成功之后再自动的将之转为一个List集合,

OK,以上两种需求用我们传统的数据库读写操作肯定都是可以实现的,只不过工作量略大,在mybatis中有一个功能略强大的typeHandler专门用来解决数据库中的数据类型和Java中的数据类型之间的转化问题,那么我们今天以上面两种需求为例,来看看typeHandler要怎么使用。

事实上,mybatis本身已经为我们提供了许多typeHandler了,系统提供的typeHandler能够满足我们日常开发中的大部分需求

官方默认存在类型处理器:https://mybatis.org/mybatis-3/zh/configuration.html#typeHandlers

如上这两种特殊的需求就需要我们自己去定义typeHandler了。

日期的转换

先来看日期的转换,假设我现在创建一张表,如下:

create table user(
    id int PRIMARY KEY auto_increment,
    username varchar(32),
    password varchar(64),
    reg_time varchar(64)
)

这张表中有一个字段叫做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,如果我不做任何特殊处理,直接向数据库插入数据,也是可以插入成功的,但是插入成功后是这样:

这个当然不是我想要的,我希望存到数据库里的是这样的:

就是我直接向数据库写数据,要写的是一个Date对象,但是写到数据库之后这个Date对象就变成了Date对象所描述的时间到1970年的秒数了,然后当我从数据库读取这个秒数之后,系统又会自动帮我将这个秒数转为Date对象,就是这样两个需求。

这个时候,我们要做的事情其实很简单,那就是自定义typeHandler,自定义typeHandler我们有两种方式,

  • 一种是实现TypeHandler接口

  • 一种简化的写法就是继承自BaseTypeHandler类

我这里先以第二种为例来进行说明。

1) 自定义typeHandler继承自BaseTypeHandler

@MappedJdbcTypes({JdbcType.VARCHAR})
@MappedTypes({Date.class})
public class DateAsVarcharTypeHandler extends BaseTypeHandler<Date> {
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Date date, JdbcType jdbcType) throws SQLException {
        preparedStatement.setString(i, String.valueOf(date.getTime()));
    }

    public Date getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return new Date(resultSet.getLong(s));
    }

    public Date getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return new Date(resultSet.getLong(i));
    }

    public Date getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return callableStatement.getDate(i);
    }
}

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

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

2) 在Mapper中进行局部配置

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

<resultMap id="userResultMap" type="org.yaorange.entity.User">
        <result typeHandler="org.yaorange.handler.DateAsVarcharTypeHandler" 
                column="reg_time" 
                javaType="java.util.Date"
                jdbcType="VARCHAR"
                property="regTime"/>
</resultMap>

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

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

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

<insert id="insertUser" parameterType="org.yaorange.entity.User">
    INSERT INTO user(username,password,reg_time) VALUES (#{username},#{password},#{regTime,javaType=Date,jdbcType=VARCHAR,typeHandler=org.yaorange.handler.DateAsVarcharTypeHandler})
</insert>

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

<insert id="insertUser" parameterType="org.yaorange.entity.User">
        INSERT INTO user(username,password,reg_time) VALUES (#{username},#{password},#{regTime,javaType=Date,jdbcType=VARCHAR})
</insert>

或者只配置typeHandler:

<insert id="insertUser3">
        INSERT INTO user(username,password,reg_time) VALUES (#{username},#{password},#{regTime,typeHandler=org.yaorange.handler.DateAsVarcharTypeHandler})
    </insert>

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

3)简单的测试:

    @Test
    public void test2() {
        SqlSession sqlSession = null;
        try {
            sqlSession = MybatisUtils.openSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = new User();
            user.setUsername("wangwu");
            user.setPassword("ww123");
            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 = MybatisUtils.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.yaorange.handler.MyDateTypeHandler"/>
</typeHandlers>

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

<typeHandlers>
    <package name="org.yaorange.handler"/>
</typeHandlers>

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

小结

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

List集合的转换

对于List集合的转换,道理都是一样的

自定义案例需求:

数据库字段favorites是varchar类型,存储爱好,值(‘吃,喝,玩,乐’),java实体属性是List

参考代码:

实体:

package com.yaorange.entity;

import java.util.Date;
import java.util.List;

public class User_type {
    private Integer id;
    private String username;// 用户姓名

    private List<String> favorites;//爱好

       //省略getter和Setter方法
}

数据库结构:

类型处理器代码:

@MappedJdbcTypes(JdbcType.VARCHAR)//指定转换的数据库类型
@MappedTypes(List.class)//指定转换的java数据类型
public class ListAsVarcharTypeHandler implements TypeHandler<List<String>> {
    @Override
    //更新操作使用使用该方法给占位符转配参数
    public void setParameter(PreparedStatement preparedStatement, int i, List<String> paramters, JdbcType jdbcType) throws SQLException {
//        i:语句中占位符序号
//        paramters:参数值
//        默认底层:preparedStatement.setString(i,paramters);
        //将参数中传入的List<String>数据转换为自定义的字符串数据
        StringBuffer buffer = new StringBuffer();
        for (String str : paramters) {
            buffer.append(str).append(",");
        }
        preparedStatement.setString(i,buffer.toString());
    }

    @Override
    //查询时获取到字符串值转换实体需要的List<String>的逻辑
    //遍历结果集时,获取指定列数据有两种方式,要么通过列明获取,要么通过列索引获取
    public List<String> getResult(ResultSet resultSet, String columnName) throws SQLException {
        String favoritesString = resultSet.getString(columnName);// 获取数据库中字符串值
        if(favoritesString != null){
            return Arrays.asList(favoritesString.split(","));//将字符串转换拆分为数组并转换为List集合
        }
        return null;
    }

    @Override
    public List<String> getResult(ResultSet resultSet, int columnIndex) throws SQLException {
        String favoritesString = resultSet.getString(columnIndex);// 获取数据库中字符串值
        if(favoritesString != null){
            return Arrays.asList(favoritesString.split(","));//将字符串转换拆分为数组并转换为List集合
        }
        return null;
    }

    @Override
    public List<String> getResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        String favoritesString = callableStatement.getString(columnIndex);// 获取数据库中字符串值
        if(favoritesString != null){
            return Arrays.asList(favoritesString.split(","));//将字符串转换拆分为数组并转换为List集合
        }
        return null;
    }
}

全局配置文件配置:

    <typeHandlers>
        <!--注册自定义类型处理器-->
        <typeHandler handler="com.yaorange.handler.ListAsVarcharTypeHandler"/>
    </typeHandlers>

Mapper接口和映射文件:

public interface UserMapper_type {
    List<User_type> getAll();

    int addUser(User_type user);
}
<?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yaorange.mapper.UserMapper_type">
    <select id="getAll" resultType="User_type">
        select * from user_type;
    </select>

    <insert id="addUser" parameterType="User_type">
      	insert into user_type(username,favorites) values(#{username},#{favorites})
    </insert>

</mapper>

测试类代码:

public class TypeHandlerTest {
    private SqlSession sqlSession;
    @Before
    public void start() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sessionFactory.openSession();
    }
    @After
    public void end(){
        sqlSession.commit();
        sqlSession.close();
    }
    @Test
    public void addUserTest() throws IOException {
        UserMapper_type mapper = sqlSession.getMapper(UserMapper_type.class);

        User_type user_type = new User_type();
        user_type.setUsername("zhangsan");
        ArrayList<String> favorites = new ArrayList<>();
        favorites.add("吃");
        favorites.add("喝");
        favorites.add("玩");
        user_type.setFavorites(favorites);

        int i = mapper.addUser(user_type);
        if(i > 0){
            System.out.println("新增成功");
        }
    }
    
    @Test
    public void getAllTest() throws IOException {
        UserMapper_type mapper = sqlSession.getMapper(UserMapper_type.class);
        List<User_type> users = mapper.getAll();
//        System.out.println(users.get(0).getFavorites().size());
        for (User_type user : users) {
            System.out.println(user);
        }
    }
}