解决:数据库decimal类型的数据查到内存是科学计数法而不是正常的数字问题。


在Java springboot+mybatis项目里,通过sql查询user表,mapper蹭有一个方法 List<Map<String, Object>> queryData(String sql); 将user表的数据查询出来。

queryData方法的返回类型是一个List<Map<String, Object>> 类型,Map中的key是user表中的字段名,value是字段对应的值。

现在发现user表中 price 的值为 0是,通过 queryData 查询出来pricede 值变成了科学计数法。

怎么解决让他显示正常的数字而不是科学计数法。

mysql user表里有一个 price字段,类型是 decimal(12,30)。

user表的结构如下:

price 字段的类型是 decimal(25, 13)。比如 更新数据为 0:update user set price = 0;

image-20250608164703149

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `userName` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
  `age` int(11) NULL DEFAULT NULL COMMENT '年龄',
  `userAccount` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户账号',
  `userPassword` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户账号密码',
  `phone` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电话',
  `email` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `userRole` int(11) NOT NULL DEFAULT 0 COMMENT '用户角色: 0-普通用户 1-管理员用户',
  `avatarUrl` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `userStatus` int(11) NOT NULL DEFAULT 0 COMMENT '状态:0-正常',
  `gender` tinyint(4) NULL DEFAULT NULL COMMENT '性别:1-男,0-女',
  `createTime` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `isDelete` tinyint(4) NOT NULL DEFAULT 1 COMMENT '逻辑删除:1,表示未删除 0,表示已删除',
  `tags` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签列表',
  `price` decimal(25, 13) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

Mapper层

public interface UserMapper {

	List<Map<String, Object>> queryData(@param("sql") String sql);
}

Dao层

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhd.UserMapper">

    <select id="queryData" resultType="java.util.Map">
        ${sql}
    </select>
</mapper>

测试类

@SpringBootTest
public class Test01 {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testBigDecimal() {
        String sql = "select price from user";
        List<Map<String, Object>> mapList = userMapper.queryData(sql);
        System.out.println("查询结果:");
    }
}

在测试类中 mapList 查询出来的数据 price 变成了科学计数法 “0E-13”。

image-20250608165016661

原因分析

当 MySQL 的 DECIMAL(25, 13) 类型值(如 0)被 MyBatis 映射为 BigDecimal 类型时,在转换为字符串时会默认使用科学计数法表示(如 0E-30)。这是因为 BigDecimal 的 toString() 方法在值较小时会自动转为科学计数法。

当数据库中 price 的值为 0时,并且是DECIMAL(25, 13)类型,此时数据库中定义的小数位比较长,当price在表里存储的值为 0 时,实际上存储的是0.0000000000000(13位小数)。

在Java中,当使用JDBC从结果集获取这个值时,会得到一个BigDecimal对象。而BigDecimal的toString()方法在遇到小数位数很多且值为0的情况下,会使用科学计数法表示,即0E-15。

解释:

BigDecimal的toString()方法在以下情况会使用科学计数法:

- 如果值很小(绝对值小于0.001)或者很大(绝对值大于等于10^7)且小数部分有有效数字,但这里0是一个特殊情况。

- 对于0,如果它的标度(scale)是负数,则表示为0,如果标度为正数,则可能用科学计数法表示。例如,标度为15,则0会表示为0E-15。

解决方案

方案 1:在查询时直接转换(推荐)

修改 SQL 查询语句,使用 MySQL 的 CAST()FORMAT() 函数强制转换为字符串:

<!-- Mapper.xml -->
<select id="queryData" resultType="map">
  SELECT 
    ${otherColumns},
    CAST(price AS CHAR) AS price  <!-- 关键转换 -->
  FROM user
  <!-- 或使用 FORMAT(price, 30) 保留小数 -->
</select>

方案 2:自定义类型处理器

创建专用类型处理器,重写 BigDecimal 的转换逻辑:

import org.apache.ibatis.type.*;
import java.math.BigDecimal;
import java.sql.*;

// 使用注解声明处理的类型(非必须但推荐)
@MappedTypes(BigDecimal.class)
@MappedJdbcTypes(JdbcType.DECIMAL)
public class PlainBigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType) 
        throws SQLException {
        ps.setBigDecimal(i, parameter);
    }

    @Override
    public BigDecimal getNullableResult(ResultSet rs, String columnName) 
        throws SQLException {
        return rs.getBigDecimal(columnName);
    }

    // 其他必要方法...

    // 重点:覆盖结果转换逻辑
    @Override
    public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
        BigDecimal value = cs.getBigDecimal(columnIndex);
        return value != null ? value.toPlainString() : null; // 使用 toPlainString()
    }
}

注册处理器:在mybatis的配置文件里注册自定义的处理器

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="com.example.PlainBigDecimalTypeHandler"/>
</typeHandlers>

方案 3:结果集后处理(通用性强)

在 Service 层对查询结果进行格式化:

public List<Map<String, Object>> queryData(String sql) {
    List<Map<String, Object>> result = mapper.queryData(sql);
    
    // 遍历结果集格式化数值
    result.forEach(map -> map.replaceAll((key, value) -> {
        if ("price".equalsIgnoreCase(key) && value instanceof BigDecimal) {
            return ((BigDecimal) value).stripTrailingZeros().toPlainString();
        }
        return value;
    }));
    
    return result;
}

方案对比

方案优点缺点适用场景
SQL转换简单直接,无代码侵入需修改所有相关查询少量固定查询
类型处理器一劳永逸,自动处理所有映射需注册全局处理器项目中有大量DECIMAL字段
结果集处理不修改SQL/映射,灵活通用每次查询都需遍历结果动态SQL/通用查询方法

最佳实践建议

  1. 精确值场景(如金融系统):

    // 使用 setScale 控制精度
    BigDecimal value = rs.getBigDecimal("price");
    return value.setScale(30, RoundingMode.HALF_UP).stripTrailingZeros();
    
  2. 避免科学计数法的关键方法

    // 正确方式
    bigDecimal.toPlainString(); 
    
    // 错误方式(会产生科学计数法)
    bigDecimal.toString(); 
    
  3. 全局配置建议(在应用启动时):

    // 禁用科学计数法(影响所有BigDecimal)
    System.setProperty("bigdecimal.formatter", "plain");
    

示例效果

转换前(科学计数法):

{"price": 0E-30}

转换后(普通数字):

{"price": "0.000000000000000000000000000000"}

选择最适合你业务场景的方案即可彻底解决此问题。

对于通用查询方法,推荐使用方案3;

对于固定字段映射,方案2更优雅。


在 Spring Boot + MyBatis 框架中,无需传统的 mybatis-config.xml 文件,可以通过以下三种方式注册自定义类型处理器(TypeHandler)实现全局生效:

方案 1:使用 @Configuration 配置类注册(推荐)
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisConfig {

    /**
     * 注册自定义类型处理器
     */
    @Bean
    public ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return configuration -> {
            // 注册单个处理器
            configuration.getTypeHandlerRegistry().register(PlainBigDecimalTypeHandler.class);
            
            // 或注册多个处理器
            configuration.getTypeHandlerRegistry().register(new YourDateTypeHandler());
        };
    }
}

方案 2:通过 application.properties 自动扫描注册
# application.properties
# 指定自定义TypeHandler所在的包(多个包用逗号分隔)
mybatis.type-handlers-package=com.example.handlers

或者 yml

# 指定自定义TypeHandler所在的包(多个包用逗号分隔)
mybatis:
  type-handlers-package: com.example.handlers
MyBatis 会自动扫描指定包下所有实现了 TypeHandler 接口的类并注册。

方案 3:使用 @MapperScan 注解注册

在启动类或配置类中指定:

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(
    basePackages = "com.example.mapper",
    typeHandlers = {PlainBigDecimalTypeHandler.class, YourDateTypeHandler.class}
)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

完整示例:自定义 BigDecimal 处理器

步骤 1:创建类型处理器

import org.apache.ibatis.type.*;
import java.math.BigDecimal;
import java.sql.*;

// 使用注解声明处理的类型(非必须但推荐)
@MappedTypes(BigDecimal.class)
@MappedJdbcTypes(JdbcType.DECIMAL)
public class PlainBigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType) 
        throws SQLException {
        ps.setBigDecimal(i, parameter);
    }

    @Override
    public BigDecimal getNullableResult(ResultSet rs, String columnName) 
        throws SQLException {
        return rs.getBigDecimal(columnName);
    }

    // 其他必要方法...

    // 重点:覆盖结果转换逻辑
    @Override
    public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
        BigDecimal value = cs.getBigDecimal(columnIndex);
        return value != null ? value.toPlainString() : null; // 使用 toPlainString()
    }
}

步骤 2:选择一种注册方式(以配置类为例)

@Configuration
public class MyBatisConfig {
    @Bean
    public ConfigurationCustomizer typeHandlerRegistry() {
        return configuration -> 
            configuration.getTypeHandlerRegistry().register(PlainBigDecimalTypeHandler.class);
    }
}

步骤 3:验证结果

@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testDecimalFormat() {
        List<Map<String, Object>> result = userMapper.queryData("SELECT * FROM user");
        
        result.forEach(row -> {
            Object price = row.get("sale_price");
            System.out.println(price.getClass() + ": " + price);
            // 输出:java.math.BigDecimal: 0 (不再是0E-15)
        });
    }
}

关键注意事项

  1. 处理器作用范围

    • 全局注册后,所有 BigDecimal 类型的字段都会使用该处理器

    • 如需局部覆盖,可在 Mapper XML 中单独指定:

      <result column="price" property="price" 
              typeHandler="com.example.AnotherTypeHandler"/>
      
  2. 处理精度问题

    // 在处理器中保留原始精度
    private BigDecimal format(BigDecimal value) {
        if (value == null) return null;
        return value.setScale(15, RoundingMode.HALF_UP) // 保持15位小数
                   .stripTrailingZeros(); // 去除尾部零
    }
    
  3. 处理 NULL 值

    • 如果字段可能为 NULL,务必在处理器中做 null 检查
    • 使用 rs.wasNull() 可检测数据库 NULL 值
  4. 调试技巧

    @PostConstruct
    public void logRegisteredHandlers() {
        // 打印所有注册的处理器
        SqlSessionFactory factory = ... // 注入SqlSessionFactory
        factory.getConfiguration()
               .getTypeHandlerRegistry()
               .getTypeHandlers()
               .forEach(handler -> 
                   System.out.println("Registered: " + handler.getClass()));
    }
    

方案对比

注册方式优点适用场景
@Configuration 配置类灵活,可编程控制注册逻辑需要条件注册处理器的场景
mybatis.type-handlers-package配置简单,零代码处理器集中存放的简单项目
@MapperScan 注解与Mapper扫描整合小型项目快速集成

重要:避免重复注册!如果同时使用多种方式,可能导致处理器被注册多次,建议选择一种统一方式管理。

通过以上任一方式,都能让自定义类型处理器在 Spring Boot + MyBatis 项目中全局生效,解决科学计数法显示问题。

SpringBoot
JAVA-技能点
Mybatis