MyBatis插入Double类型数据时丢失精度问题求助
MyBatis映射器里的批量插入SQL如下:
<foreach collection="ps" item="p" separator=";" open="" close=""> INSERT INTO ${stageTableName} VALUES( #{p.v1}, #{p.v2}, #{p.v3}, #{p.v4}, #{p.v5}, #{vx}, #{p.quantity} ) ON CONFLICT ("v1", "v2", "v3", "v4", "v5") DO UPDATE SET quantity = ${stageTableName}.quantity + EXCLUDED.quantity </foreach>
ps是Java对象列表,之前运行都正常,但最近碰到个问题:当p.quantity的值是1222371636058.8174时,数据库存下来变成了1222371636058.82,精度丢了。
已排查信息
- 调试确认精度丢失是在MyBatis插入环节发生的
- 直接把这个数值硬编码到SQL里执行,Postgres能正确存储
1222371636058.8174 - 只有这种大数值会出问题,像
123.8124872这类小数值存储精度完全正常
环境信息
- Java对象中
p.quantity的类型:Double - 数据库
quantity字段类型:NUMERIC(38,7) - 数据库:Postgres
问题根源在于Java的Double类型没办法精确表示所有十进制小数。Double是遵循IEEE 754标准的双精度浮点数,它用二进制存储数值,当十进制数的整数部分位数较多时(比如你这个案例里整数部分有12位),剩下的二进制位数不足以精确存储所有小数位。1222371636058.8174这个值在Double里实际存的是近似值,刚好就是1222371636058.82左右,MyBatis把这个近似值传给数据库,自然就丢了精度。
小数值的整数部分位数少,二进制有足够空间存下所有小数细节,所以不会出问题。另外MyBatis处理Double参数时,直接传的是浮点数的近似值,不会自动转成高精度十进制类型,这就把Double的精度缺陷放大了。
1. 改Java对象字段类型(最优方案)
把p.quantity的类型从Double换成java.math.BigDecimal,这是Java专门用来精确表示十进制数的类型,能完整保留所有小数位的信息,从根源上解决精度问题。
2. 适配MyBatis配置
改完Java类型后,MyBatis默认支持BigDecimal参数绑定,会自动把它以高精度十进制类型传给Postgres,完美匹配NUMERIC(38,7)的字段类型,原SQL里的#{p.quantity}不用做任何改动。
临时规避方案(不推荐)
如果暂时没法改Java类型,可以在MyBatis映射里显式指定参数的JDBC类型为DECIMAL,强制MyBatis把Double转成高精度十进制再传递:
#{p.quantity, jdbcType=DECIMAL}
但这种方式只是临时救急,因为Double本身已经存在精度损失了,不如直接换BigDecimal彻底解决问题。
内容的提问来源于stack exchange,提问作者Ram

