MyBatis + PostgreSQL 使用 JSONB 类型踩坑实录:column is of type jsonb 报错解决方案

一、问题背景

后端框架:Spring Boot 2.x+ MyBatis-Plus 3.5.2
数据库:PostgreSQL,部分字段使用 jsonb 存储结构化数据

数据库表结构 :
CREATE TABLE “bpm_record_form_data”(
“id” serial4 NOT NULL,
“curr_time” timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
“record_id” int4,
“data” jsonb,
CONSTRAINT “bpm_record_form_data_pkey” PRIMARY KEY (“id”)
);
COMMENT ON COLUMN “bpm_record_form_data”.“id” IS ‘ID’;
COMMENT ON COLUMN “bpm_record_form_data”.“curr_time” IS ‘最后修改时间’;
COMMENT ON COLUMN “bpm_record_form_data”.“data” IS ‘表单数据’;
COMMENT ON COLUMN “bpm_record_form_data”.“record_id” IS ‘发起项目ID’;
COMMENT ON TABLE “bpm_record_form_data” IS ‘表单解析数据表’;

出现的错误:

17:30:56.402 [main] ERROR druid.sql.Statement - [statementLogError,148] - {conn-110005, pstmt-120001} execute error. INSERT INTO bpm_record_form_data ( record_id,
data ) VALUES ( ?,
? )
org.postgresql.util.PSQLException: ERROR: column “data” is of type jsonb but expression is of type character varying
建议:You will need to rewrite or cast the expression.
位置:72

二、错误原因分析

  1. 从 PostgreSQL 角度解释:
    jsonb 是 PostgreSQL 的强类型字段,不能自动将字符串转换为 jsonb
    MyBatis 默认会把 Java 对象当作字符串处理(varchar)
  2. 从 JDBC/MyBatis 角度分析:
    插入时执行的是 setString(…),会导致类型不匹配
    正确的做法应该是 setObject(…, Types.OTHER),让 PostgreSQL 识别为 jsonb
  3. 感兴趣可以断点调试下org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings
    核心逻辑就是遍历未显式映射的列,尝试通过 TypeHandlerRegistry 查找合适的 TypeHandler,从而完成自动映射。

三、解决方案

  1. 使用自定义 TypeHandler
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

import java.sql.*;

@MappedTypes({Object.class})
@MappedJdbcTypes(JdbcType.OTHER)
public class JsonbTypeHandler<T> extends BaseTypeHandler<T> {
    private static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter("com.扫描包");
    private Class<T> clazz;

    public JsonbTypeHandler() {
	    super();
    }

    public JsonbTypeHandler(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, JSON.toJSONString(parameter, JSONWriter.Feature.WriteClassName), Types.OTHER);
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        if (json == null) return null;

        return JSON.parseObject(json, clazz, AUTO_TYPE_FILTER);
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        if (json == null) return null;

        return JSON.parseObject(json, clazz, AUTO_TYPE_FILTER);
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        if (json == null) return null;

        return JSON.parseObject(json, clazz, AUTO_TYPE_FILTER);
    }
}


这里是用的fastjson2

<dependency>
  <groupId>com.alibaba.fastjson2</groupId>
   <artifactId>fastjson2</artifactId>
   <version>2.0.41</version>
</dependency>
  1. 实体类使用注解指定 TypeHandler
@Accessors(chain = true)
@Data
@NoArgsConstructor
@TableName("bpm_record_form_data")
public class BpmRecordFormData implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * ID
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    @TableId
    private Long id;
    /**
     * 发起项目ID
     */
    private Long recordId;
    /**
     * 最后修改时间
     */
    @JsonSerialize(using = CustomLocalDateTimeSerializer.class)
    @JsonDeserialize(using = CustomLocalDateTimeDeserializer.class)
    private LocalDateTime currTime;

    /**
     * 表单数据
     */
    @TableField(value = "data", typeHandler = JsonbTypeHandler.class)
    private List<FormAnalysisModuleData> data;

}

3.MyBatis 配置注册 TypeHandler

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        /*
        自动分页: PaginationInnerInterceptor
        多租户: TenantLineInnerInterceptor
        动态表名: DynamicTableNameInnerInterceptor
        乐观锁: OptimisticLockerInnerInterceptor
        sql 性能规范: IllegalSQLInnerInterceptor
        防止全表更新与删除: BlockAttackInnerInterceptor
        */
        // 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
        //interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            configuration.getTypeHandlerRegistry().register(List.class, JdbcType.OTHER, JsonbTypeHandler.class);
            configuration.getTypeHandlerRegistry().register(Map.class, JdbcType.OTHER, JsonbTypeHandler.class);
            configuration.getTypeHandlerRegistry().register(Object.class, JdbcType.OTHER, JsonbTypeHandler.class);

        };
    }


}

为什么必须注册 TypeHandler?
MyBatis-Plus 的 getById() 等通用方法,并不会读取你实体类字段上的 @TableField(typeHandler = …) 注解;
它依赖的是 MyBatis 全局的 TypeHandlerRegistry;
如果你没有注册 JsonbTypeHandler 到 Object.class 的处理器,它会使用默认的 StringTypeHandler 去尝试解析 PostgreSQL 的 jsonb 字段,结果就是返回原始 JSON 字符串甚至抛异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬砖的小庄啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值