JDBC驱动数据类型解析

有个程序本来是在Sqlserver下运行的,写的SQL也是标准的T-SQL语句,没有特殊语法。某一天切换成了Oracle,本来信心满满认为那指定不会有问题,又没什么特殊语法嘛,但是刚切换执行,登录就报错了o(╯□╰)o .....错误信息显示竟然是一个简单的select count(1) from x_table where lname='xx'报错了。java.lang.ClassCastException: java.math.BigDecimal cannot be cast to java.lang.Integer

  1. 异常具体情形复现

    根据控制台的错误堆栈信息,很容易就能定位到错误,并且根据代码行数追踪到具体的代码

    //异常信息:
    java.lang.ClassCastException: java.math.BigDecimal cannot be cast to java.lang.Integer
    	at com.loo.bd.pub.service.RegService.cancelCc(RegService.java:284)
    	at com.loo.bd.pub.service.RegService$$FastClassBySpringCGLIB$$5b3e2d7e.invoke(<generated>)
    
    //代码片段:Mybatis的接口和对应的xml
    	Map<String,Integer> queryLoginLogTimes();
    //一个很简单的查询语句
    	<select id="queryLoginLogTimes" parameterType="java.lang.String" resultType="java.util.Map">
    			select count(1) times,max(login_date) last_date from user_log where pk_user = #{pkUser,JdbcType=VARCHAR}
    	</select>
    	
    //service部分代码如下
      Map<String,Object> userLogMap = userMapper.queryLoginLogTimes(login.getPkuser());
      if(userLogMap!=null && userLogMap.size()>0){
        Integer currentTimes =  Integer(userLogMap.get("times"));//:284行就这里哦
      }
    
  2. 原因分析和解决问题

    对于count函数的返回类型不同

    在SQLServer中count函数的返回数据类型是int,在Oracle中count函数返回的类型是Number。而对应JDBC驱动的实现中,映射Java的数据类型一个就是int,一个则是BigDecimal。

    编码不规范导致驱动按照默认类型处理

    上面的编码实例中,没有使用Mybatis的ResultMap映射结果集,而是直接使用了一个Map对象来接受,这样依赖底层处理类型接受,读取列的类型映射关系完全按照jdbc驱动包的实现来处理。而每一个厂商实现的驱动包默认的类型就会有所不同。

    正确的姿势:
    a. 要么使用 ResultMap对结果集做映射,让Mybatis读取数据的时候按照指定类型读取(其实就是jdbc中的ResultSet中的getXXX方法,按照不同类型读取)。

      <resultMap id="loginTimeMap" type="com.loo.bd.LoginLog">
          <result column="times" property="times" javaType="Integer"/>
          <result column="last_date" property="lastDate" javaType="Date" />
      </resultMap>
      <select id="queryLoginLogTimes" parameterType="java.lang.String" resultMap="loginTimeMap">
    			select count(1) times,max(login_date) last_date from user_log where pk_user = #{pkUser,JdbcType=VARCHAR}
    	</select>
    

    b. 当然如果,考虑返回的是一个值,比如上面的语句,如果只是返回一个count统计,可以直接指定resultType="int"即可。这样无论在哪个数据库下返回也是int,毕竟单个count永远不会返回null

    c. 当然使用MybatisPlus也行,或者其他持久化映射框架,总之不要返回一个map,尤其是返回一个类型不确定的结果,都有问题。

    错误姿势和理解:

    有人可能会想,那我如果确定返回类型了,比如确定所有列返回都是int了,我直接采用Map<String,Integer>在Mapper接口中定义接受不就完了。像这样Map<String,Integer> userLogMap = userMapper.queryLoginLogTimes(login.getPkuser()); 这样编译是不会报错,但是运行依然是类型转换异常。Mybatis的底层运行时返回的其实是Object类型,最终返回的时候只是判断 目标类型是不是返回结果类型或者其接口、超类类型。也就是并不关注具体的泛型类型,具体泛型类型的准确性是需要程序员自己判定和映射。这里的处理姿势与Java是弱泛型有关系,Java的泛型是运行时擦除的,简单说就是虽然我们定义的是Map<String,Integer>但实际上,运行时就是个Map.

    如下是Mybatis底层对于类型的判断

    // 类似上面使用到的查询,具体方法会走到MapperMethod中的executeForMany判断类型能匹配,并不关注泛型类型。
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
          if (method.getReturnType().isArray()) {
            return convertToArray(result);
          } else {
            return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
          }
        }
    
  3. 默认类型到底从何而来

    对于Java程序而言,大多情况下访问数据库都是通过各个厂商按照jdbc规范自己实现的驱动jar来访问和操作具体的数据库,那么这些数据类型,以及数据库数据类型和Java数据类型的映射关系都是在驱动包中处理的。

    各驱动包依据jdbc规范中ResultSetMetaData接口,实现自己的元数据处理类,其中就包含了返回列的‘数据库数据类型’、‘Java数据类型’,以及列名称、大小等等都在此接口的实现中。比如Oracle对于数据类型映射的实现,对应的数据库类型映射是实现规范接口的getColumnTypeName方法,可以看到6代表的是“Number”<>"java.math.BigDecimal"之间映射。

    一个获取列数据类型,默认映射的代码:

    try(Connection conn = getConnection();
                PreparedStatement pstmt = conn.prepareStatement("select * from TEST_TB");
                ResultSet resultSet = pstmt.executeQuery()){
                ResultSetMetaData metaData = resultSet.getMetaData();
                while (resultSet.next()){
                    for (int i = 1; i <= metaData.getColumnCount(); i++) {
                        System.out.println(metaData.getColumnName(i) + "\t" +metaData.getColumnTypeName(i)+ "\t" +metaData.getColumnDisplaySize(i) +"\t" + resultSet.getObject(i).getClass());
                    }
                }
            }
    
  4. 总结

    通常情况下,我们采用一些持久层框架,规范化代码实现,都会指定具体的数据类型,避免了以上问题的发生,因此大多人对此类问题并无感知,也不知道为何会如此。(工具的好处和坏处!)

    使用框架包装,不指定具体类型,就会按照Object类型去读去处理,而按照Object类型,就会使用到驱动层面实现的默认类型映射。

    比如Oracle对于int、Number类型的映射都是java.math.BigDeciml、Sqlserver对于int类型映射为Integer,numeric映射为java.math.BigDecimal

posted @ 2024-10-09 09:33  冰雪女娲  阅读(64)  评论(0)    收藏  举报