MyBatis之typeHandlers

一、案例分析

在日常开发中,有不少对日期类型的操作。比如订单时间、付款时间等,通常这一类数据在数据库以datetime类型保存。如果需要在页面上展示此值,在Java中以什么类型接收它呢?在不执行任何二次操作的情况下: 用java.util.Date接收,在页面展示的就是Tue Oct 16 16:05:13 CST 2018。 用java.lang.String接收,在页面展示的就是2018-10-16 16:10:47.0。显然,我们不能显示第一种。第二种似乎可行,但大部分情况下不能出现毫秒数。当然了,不管哪种方式,在显示的时候format一下当然是可行的。有没有更好的方式呢?

二、typeHandlers

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。 在数据库中,datetime和timestamp类型含义是一样的,不过timestamp存储空间小, 所以它表示的时间范围也更小。 下面来看几个Mybatis默认的时间类型处理器。
JDBC 类型 Java 类型 类型处理器
DATE java.util.Date DateOnlyTypeHandler
DATE java.sql.Date SqlDateTypeHandler
DATE java.time.LocalDate LocalDateTypeHandler
DATE java.time.LocalTime LocalTimeTypeHandler
TIMESTAMP java.util.Date DateTypeHandler
TIMESTAMP java.time.Instant InstantTypeHandler
TIMESTAMP java.time.LocalDateTime LocalDateTimeTypeHandler
TIMESTAMP java.sql.Timestamp SqlTimestampTypeHandler
  如果数据库字段类型为JDBC 类型,同时Java字段的类型为Java 类型,那么就调用类型处理器类型处理器。

三、自定义处理器

基于上面这个逻辑,我们可以增加一种处理器来处理我们开头所描述的问题。我们可以在Java中,以String类型接收数据库的DateTime类型数据。因为现在的接口以restful风格居多,用String类型方便传输。 最后的毫秒数通过自定义的处理器统一截取去除即可。
JDBC 类型 Java 类型 类型处理器
TIMESTAMP java.lang.String CustomTypeHandler
<property name="typeHandlers">
    <array>
        <bean class="com.viewscenes.netsupervisor.util.CustomTypeHandler"></bean>
    </array>
</property> 
@MappedJdbcTypes注解表示JDBC的类型,@MappedTypes表示Java属性的类型。
@MappedJdbcTypes({ JdbcType.TIMESTAMP })
@MappedTypes({ String.class })
public class CustomTypeHandler extends BaseTypeHandler<String>{ 
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setString(i, parameter);
    }
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return substring(rs.getString(columnName));
    }
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return rs.getString(columnIndex);
    }
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
    private String substring(String value) {
        if (!"".endsWith(value) && value != null) {
            return value.substring(0, value.length() - 2);
        }
        return value;
    }
} 
  通过以上方式,我们就可以放心的在Java中以String接收数据库的时间类型数据了。

四、源码分析

1、注册

public final class TypeHandlerRegistry {
    //typeHandler为当前自定义类型处理器
    public <T> void register(TypeHandler<T> typeHandler) {
        boolean mappedTypeFound = false;
        //mappedTypes即String
        MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
        if (mappedTypes != null) {
            for (Class<?> handledType : mappedTypes.value()) {
                register(handledType, typeHandler);
            }
        }
    }
}
public final class TypeHandlerRegistry {
    private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
        //JDBC的类型,即TIMESTAMP
        MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().
                getAnnotation(MappedJdbcTypes.class);
        if (mappedJdbcTypes != null) {
            for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
                //TYPE_HANDLER_MAP是Java类型中的默认处理器。
                //以String为例,它默认可以处理VARCHAR、CHAR、NVARCHAR、CLOB、NCLOB、NULL
                Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
                //给String添加一种处理器为typeHandler
                map.put(jdbcType, typeHandler);
                //注册处理器实例
                ALL_TYPE_HANDLERS_MAP.put(typeHandler.getClass(), typeHandler);
            }
        }
    }
} 

2、调用

注册完毕之后,它在什么地方生效呢?关键在于能否可以找到这个处理器。看完上面的注册过程,查找其实很简单。先从TYPE_HANDLER_MAP根据JavaType,获取String类型的全部处理器,再从中过滤出JDBC类型为TIMESTAMP的即可。
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    //根据JavaType获取String类型的全部处理器
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    TypeHandler<?> handler = null;
    if (jdbcHandlerMap != null) {
        //再根据jdbcType获取到TIMESTAMP的处理器
        handler = jdbcHandlerMap.get(jdbcType);
    }
    return (TypeHandler<T>) handler;
} 
拿到自定义的处理器,就可以自定义处理。不过,在Mybatis-3.2.7版本中,在调用getTypeHandler方法时,它并没有传jdbcType这个参数,所以这个参数默认为NULL了。 那么,在执行jdbcHandlerMap.get(jdbcType)的时候,会找不到自定义的处理器,而是找到了NULL的处理器,即StringHandler。代码如下:
public class ResultSetWrapper {
    public TypeHandler<?> getTypeHandler(Class<?> propertyType, String columnName) {
        //3.4.6
        JdbcType jdbcType = getJdbcType(columnName);
        handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
        //3.2.7
        handler = typeHandlerRegistry.getTypeHandler(propertyType);
    }
} 

五、总结

自定义处理器的应用场景很广泛,比如对某些敏感字段加密、状态值的转换(正常、注销、 已付款、未发货)等,可以考虑用它来做。

六、后续

在写完这篇文章后,在另外一台电脑做测试的时候,发现尽管没有对时间类型做处理,但也不会出现.0的问题。于是决定先抛开Mybatis,用最原始的JDBC做测试来。
public static void main(String[] args) throws Exception {
    Connection conn = getConnection();
    Statement stat = conn.createStatement();
    String sql = "select * from user";
    ResultSet rs = stat.executeQuery(sql);
    while(rs.next()){
        String username = rs.getString("username");
        String createtime = rs.getString("createtime");
        System.out.print("姓名: " + username);
        System.out.print("  创建时间: " + createtime);
        System.out.print("\n");
    }
} 
结果很意外,用原始的JDBC查询数据,并没有任何其他操作,也没有.0的问题。
姓名: 关小羽 创建时间: 2018-10-15 17:04:11
姓名: 小露娜 创建时间: 2018-10-15 17:10:46
姓名: 亚麻瑟 创建时间: 2018-10-15 17:10:46
姓名: 小鲁班 创建时间: 2018-10-16 16:10:47 
上面的代码量很小,显然问题出在ResultSet对象上。通过跟踪源码,发现两台机器的mysql-connector-java版本不一样。一个是5.1.31,一个是6.0.6。把版本换成5.1.31,执行上面的main方法再看结果。 
姓名: 关小羽 创建时间: 2018-10-15 17:04:11.0
姓名: 小露娜 创建时间: 2018-10-15 17:10:46.0
姓名: 亚麻瑟 创建时间: 2018-10-15 17:10:46.0
姓名: 小鲁班 创建时间: 2018-10-16 16:10:47.0 
那看看它们的差别在哪里,原来5.1.31多做了一步操作,它针对时间类型的数据又处理了一次,导致问题产生。

5.1.31

package com.mysql.jdbc;
public class ResultSetImpl implements ResultSetInternalMethods {
    protected String getStringInternal(int columnIndex, boolean checkDateTypes)
        // JDBC is 1-based, Java is not !?
        int internalColumnIndex = columnIndex - 1;
        Field metadata = this.fields[internalColumnIndex];      
        String stringVal = null;    
        String encoding = metadata.getCharacterSet();
        //stringVal为已经从数据库取到的值2018-10-16 16:10:47
        stringVal = this.thisRow.getString(internalColumnIndex, encoding, this.connection);
        
        // Handles timezone conversion and zero-date behavior
        //Mysql针对时间类型又做了一次处理
        if (checkDateTypes && !this.connection.getNoDatetimeStringSync()) {
            switch (metadata.getSQLType()) {
            case Types.TIME:
                ......略
            case Types.DATE:
                ......略
            case Types.TIMESTAMP:
                //数据库的DateTime类型会走到这里
                //MySQL把它又转成了Timestamp类型,  .0的问题从这里产生
                Timestamp ts = getTimestampFromString(columnIndex,
                        null, stringVal, this.getDefaultTimeZone(), false);
                return ts.toString();
            default:
                break;
            }
        }
        return stringVal;
    }
} 

6.0.6

public class ResultSetImpl extends MysqlaResultset 
                implements ResultSetInternalMethods, WarningListener {
    
    public String getString(int columnIndex) throws SQLException {
        
        Field f = this.columnDefinition.getFields()[columnIndex - 1];
        ValueFactory<String> vf = new StringValueFactory(f.getEncoding());
        // return YEAR values as Dates if necessary
        if (f.getMysqlTypeId() == MysqlaConstants.FIELD_TYPE_YEAR && this.yearIsDateType) {
            vf = new YearToDateValueFactory<>(vf);
        }
        String stringVal = this.thisRow.getValue(columnIndex - 1, vf);

        return stringVal;
    }
} 
如果项目里面有.0问题产生,可以通过升级mysql-java版本解决。如果不能动版本,可以考虑自定义的类型处理器。

原文地址:https://www.cnblogs.com/johnvwan/p/15646146.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


1.pom.xml引入依赖 &lt;dependency&gt; &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt; &lt;artifactId&gt;pagehelper&lt;/artifactId&gt; &lt;version&gt;5
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt; &lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &qu
准备工作 ① 创建数据库&amp;数据表 ## 创建数据库 CREATE DATABASE `dbtest1`; ## 创建数据表 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(20) DEF
MyBatis逆向工程是指根据数据库表结构自动生成对应的实体类、Mapper接口以及SQL映射文件的过程。这个过程可以通过MyBatis提供的逆向工程工具来完成,极大地方便了开发人员,避免了重复的代码编写,提高了开发效率。 创建逆向工程的步骤 1、添加依赖&amp;插件 &lt;!-- 控制Mave
MyBatis获取参数值的两种方式:${}和#{} ${}的本质就是字符串拼接,#{}的本质就是占位符赋值。 ${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自
resultMap作用是处理数据表中字段与java实体类中属性的映射关系。 准备工作 ① 创建数据库&amp;数据表 CREATE DATABASE `dbtest1`; CREATE TABLE `t_emp` ( `emp_id` int NOT NULL AUTO_INCREMENT, `em
EHCache缓存针对于MyBatis的二级缓存。 MyBatis默认二级缓存是SqlSessionFactory级别的。 添加依赖 &lt;!-- MyBatis-EHCache整合包 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.mybatis.cac
MyBatis 提供了一级缓存和二级缓存的支持,用于提高数据库查询的性能,减少不必要的数据库访问。 一级缓存(SqlSession 级别的缓存) 一级缓存是 MyBatis 中最细粒度的缓存,也称为本地缓存。它存在于每个 SqlSession 的生命周期中,当 SqlSession 被关闭或清空时,
动态SQL是 MyBatis 中非常强大且灵活的功能,允许你根据不同的条件构建SQL查询。 这主要通过 &lt;if&gt;、&lt;choose&gt;、&lt;when&gt;、&lt;otherwise&gt;、&lt;foreach&gt;等标签实现。 查询场景 /** * 根据条件查询员工
本教程操作系统:windows10系统、DELL G3电脑。 MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。在 MyBatis 中,配置数据库连接是非常重要的第一步。下面将详细介绍如何配置 MyBatis 的
今天小编给大家分享的是MyBatis批量查询、插入、更新、删除如何实现,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。
今天小编给大家分享的是Mybatis操作多数据源实现的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇文章和大家了解一下mybatis集成到spring的方式有哪些。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1 前言1.1 集成spring前使用mybat...
今天小编给大家分享的是mybatis-plus分页查询的3种方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇内容主要讲解“mybatis之BaseTypeHandler怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“mybatis...
这篇文章主要介绍了mybatisforeach怎么传两个参数的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇mybatisforeach怎...
这篇“MyBatis映射文件中parameterType与resultType怎么使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的...
这篇文章主要介绍“MyBatis怎么获取自动生成的键值”,在日常操作中,相信很多人在MyBatis怎么获取自动生成的键值问题上存在疑惑,小编查阅了各式资料,整理出
这篇文章主要讲解了“怎么去掉IntelliJIDEA中mybatis对应的xml文件警告”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入...
这篇文章主要介绍“MybatisPlus使用@TableId主键id自增长无效如何解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这...