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
异常具体情形复现
根据控制台的错误堆栈信息,很容易就能定位到错误,并且根据代码行数追踪到具体的代码
原因分析和解决问题
对于 count 函数的返回类型不同
在 SQLServer 中 count 函数的返回数据类型是 int,在 Oracle 中 count 函数返回的类型是 Number。而对应 JDBC 驱动的实现中,映射 Java 的数据类型一个就是 int,一个则是 BigDecimal。
编码不规范导致驱动按照默认类型处理
上面的编码实例中,没有使用 Mybatis 的 ResultMap 映射结果集,而是直接使用了一个 Map 对象来接受,这样依赖底层处理类型接受,读取列的类型映射关系完全按照 jdbc 驱动包的实现来处理。而每一个厂商实现的驱动包默认的类型就会有所不同。
正确的姿势:
要么使用 ResultMap 对结果集做映射,让 Mybatis 读取数据的时候按照指定类型读取(
其实就是jdbc中的ResultSet中的getXXX方法
,按照不同类型读取)
当然如果,考虑返回的是一个值,比如上面的语句,如果只是返回一个 count 统计,可以直接指定 resultType="int"即可。这样无论在哪个数据库下返回也是 int,毕竟单个 count 永远不会返回 null
当然使用 MybatisPlus 也行,或者其他持久化映射框架,总之不要返回一个 map,尤其是返回一个类型不确定的结果,都有问题。
错误姿势和理解:
有人可能会想,那我如果确定返回类型了,比如确定所有列返回都是 int 了,我直接采用 Map<String,Integer>在 Mapper 接口中定义接受不就完了。
像这样这样编译是不会报错,但是运行依然是类型转换异常。Mybatis 的底层运行时返回的其实是 Object 类型,最终返回的时候只是判断 目标类型是不是返回结果类型或者其接口、超类类型。也就是并不关注具体的泛型类型,具体泛型类型的准确性是需要程序员自己判定和映射。这里的处理姿势与 Java 是弱泛型有关系,Java 的泛型是运行时擦除的,简单说就是虽然我们定义的是 Map<String,Integer>但实际上,运行时就是个 Map.如下是 Mybatis 底层对于类型的判断:
默认类型到底从何而来
对于 Java 程序而言,大多情况下访问数据库都是通过各个厂商按照 jdbc 规范自己实现的驱动 jar 来访问和操作具体的数据库,那么这些数据类型,以及数据库数据类型和 Java 数据类型的映射关系都是在驱动包中处理的。
各驱动包依据 jdbc 规范中
ResultSetMetaData
接口,实现自己的元数据处理类,其中就包含了返回列的‘数据库数据类型’、‘Java 数据类型’,以及列名称、大小等等都在此接口的实现中。比如 Oracle 对于数据类型映射的实现,对应的数据库类型映射是实现规范接口的getColumnTypeName
方法,可以看到 6 代表的是“Number”<>"java.math.BigDecimal"之间映射。
一个获取列数据类型,默认映射的代码:
总结
通常情况下,我们采用一些持久层框架,规范化代码实现,都会指定具体的数据类型,避免了以上问题的发生,因此大多人对此类问题并无感知,也不知道为何会如此。(工具的好处和坏处!)
使用框架包装,不指定具体类型,就会按照 Object 类型去读去处理,而按照 Object 类型,就会使用到驱动层面实现的默认类型映射。
比如 Oracle 对于 int、Number 类型的映射都是 java.math.BigDeciml、Sqlserver 对于 int 类型映射为 Integer,numeric 映射为 java.math.BigDecimal
版权声明: 本文为 InfoQ 作者【豆芽发达咯】的原创文章。
原文链接:【http://xie.infoq.cn/article/f8f8dbc2bbb9a76a7c4a39b76】。文章转载请联系作者。
评论