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
-
异常具体情形复现
根据控制台的错误堆栈信息,很容易就能定位到错误,并且根据代码行数追踪到具体的代码
//异常信息: 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行就这里哦 } -
原因分析和解决问题
对于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); } } -
默认类型到底从何而来
对于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()); } } } -
总结
通常情况下,我们采用一些持久层框架,规范化代码实现,都会指定具体的数据类型,避免了以上问题的发生,因此大多人对此类问题并无感知,也不知道为何会如此。(工具的好处和坏处!)
使用框架包装,不指定具体类型,就会按照Object类型去读去处理,而按照Object类型,就会使用到驱动层面实现的默认类型映射。
比如Oracle对于int、Number类型的映射都是java.math.BigDeciml、Sqlserver对于int类型映射为Integer,numeric映射为java.math.BigDecimal

浙公网安备 33010602011771号