Hibernate+H2数据库因Schema创建方式导致BigDecimal返回值不一致
这个问题我之前也碰到过,本质是Hibernate和Liquibase生成的H2字段定义虽然表面参数一致,但实际底层有差异,或者H2返回的BigDecimal的scale不同导致字符串表现不一样。给你几个可行的解决方向:
你提到实体字段标注的是precision = 4, scale = 16,这里要注意:H2的DECIMAL(p,s)要求总精度p必须大于等于小数位s,否则H2会自动调整p的值(比如把p设为s+1)。而Hibernate在生成Schema时,可能会自动修正这个不合理的参数(比如把p调整为20),但Liquibase会严格按照你写的参数生成,导致两者实际的字段定义不一样。
解决步骤:
- 启动用Hibernate create-drop的应用,打开H2控制台查看表的字段类型,比如实际生成的是
DECIMAL(20,16)还是其他。 - 在Liquibase的changelog里,把对应的字段precision改成和Hibernate生成的一致,比如如果是20,就写
<column name="xxx" type="DECIMAL(20,16)"/>或者用precision="20" scale="16"。
这样两者的Schema完全一致,返回的BigDecimal格式自然就统一了。
如果不想调整Schema,可以通过Hibernate的属性转换器,把读取到的BigDecimal统一转成相同的格式:
写一个全局生效的转换器:
import javax.persistence.AttributeConverter; import javax.persistence.Converter; import java.math.BigDecimal; @Converter(autoApply = true) public class BigDecimalNormalizer implements AttributeConverter<BigDecimal, BigDecimal> { @Override public BigDecimal convertToDatabaseColumn(BigDecimal attribute) { return attribute; } @Override public BigDecimal convertToEntityAttribute(BigDecimal dbData) { if (dbData == null) { return null; } // 转成最简形式,去掉末尾的0和多余的小数点 return dbData.stripTrailingZeros(); } }
这个转换器会自动把所有BigDecimal类型的字段转成最简格式,不管数据库返回的是420还是420.000000000000,最终实体里的BigDecimal调用toString()后都会是"420"。如果想统一保留小数位,把stripTrailingZeros()改成dbData.setScale(16)即可。
如果只是测试需要一致,也可以不修改代码或Schema,而是调整测试里的断言方式:
不要直接断言返回的字符串是"420",而是断言BigDecimal的数值相等。比如用assertEquals(0, new BigDecimal("420").compareTo(返回的BigDecimal))——因为BigDecimal.compareTo()只比较数值,不考虑scale;而equals()会同时比较数值和scale,这也是之前测试失败的原因之一。
这样不管返回的是420还是420.000000000000,只要数值正确,测试就会通过。
内容的提问来源于stack exchange,提问作者Rollo Tomassi

