You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring CRUDRepository在Postgres中保存嵌套JSON列报错求助

嘿,我之前也踩过Hibernate+Postgres JSON存储的坑,给你梳理下可能的问题和解决办法,应该能帮到你~

问题分析与解决步骤

1. 先确认数据库基础配置

首先得确保Postgres里对应inputs的字段类型是**jsonb**(优先推荐,支持索引和高效查询)或者json。如果字段类型是普通的varchar或者其他类型,肯定会触发序列化/存储错误,这是最容易忽略的基础项。

2. 检查自定义Hibernate UserType的实现(如果用了自定义类型)

自定义UserType是核心,几个关键方法必须写对:

  • nullSafeGet:从数据库读取JSON字符串,反序列化为你的自定义实体列表
  • nullSafeSet:把实体列表序列化为JSON字符串,写入数据库
  • sqlTypes()要返回适配Postgres JSON的类型码

给你一个靠谱的示例实现:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.type.SerializationException;
import org.hibernate.usertype.UserType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;

public class JsonListUserType implements UserType {

    // 最好用Spring容器注入的ObjectMapper,避免配置不一致
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public int[] sqlTypes() {
        return new int[]{Types.OTHER}; // 适配Postgres的json/jsonb类型
    }

    @Override
    public Class<?> returnedClass() {
        return List.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) return true;
        if (x == null || y == null) return false;
        return x.equals(y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
        String json = rs.getString(names[0]);
        if (json == null) return null;
        try {
            // 替换成你的自定义输入实体类
            return objectMapper.readValue(json, 
                objectMapper.getTypeFactory().constructCollectionType(List.class, YourCustomInputEntity.class));
        } catch (Exception e) {
            throw new SerializationException("反序列化JSON失败: " + json, e);
        }
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
        if (value == null) {
            st.setNull(index, Types.OTHER);
            return;
        }
        try {
            String json = objectMapper.writeValueAsString(value);
            st.setObject(index, json, Types.OTHER);
        } catch (Exception e) {
            throw new SerializationException("序列化对象为JSON失败", e);
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) return null;
        try {
            String json = objectMapper.writeValueAsString(value);
            return objectMapper.readValue(json, returnedClass());
        } catch (Exception e) {
            throw new SerializationException("深拷贝对象失败", e);
        }
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) deepCopy(value);
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return deepCopy(cached);
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return deepCopy(original);
    }
}

3. 实体类的字段映射配置

在Transaction实体里,给inputs字段加上正确的注解:

import javax.persistence.*;
import org.hibernate.annotations.Type;
import java.util.List;

@Entity
@Table(name = "transactions")
public class Transaction {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String hash;

    // 指定自定义UserType,同时声明数据库字段类型为jsonb
    @Type(type = "com.yourpackage.JsonListUserType")
    @Column(columnDefinition = "jsonb")
    private List<YourCustomInputEntity> inputs;

    // 构造器、getter、setter省略
}

更简单的替代方案(推荐)

如果你的Spring Boot版本比较新(2.4+),完全可以不用自定义UserType,直接用Hibernate自带的@JdbcTypeCode注解,自动用Jackson处理序列化:

import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "transactions")
public class Transaction {

    // ...其他字段

    @Column(columnDefinition = "jsonb")
    @JdbcTypeCode(SqlTypes.JSON)
    private List<YourCustomInputEntity> inputs;

    // ...
}

这种方式减少自定义代码,避免很多潜在的坑。

4. 检查Jackson配置(关键)

确保Jackson能正确序列化/反序列化你的自定义实体:

  • 实体类要有无参构造器(Jackson默认需要)
  • 如果有特殊类型(比如Java 8日期、枚举),要给Jackson配置对应的模块:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        // 处理Java 8日期类型
        mapper.registerModule(new JavaTimeModule());
        mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return mapper;
    }
}

5. Spring Data Repository的使用

确保你的Repository定义正确,调用save()方法时没有额外的错误:

import org.springframework.data.repository.CrudRepository;

public interface TransactionRepository extends CrudRepository<Transaction, Long> {
}

保存示例:

@Service
public class TransactionService {

    private final TransactionRepository transactionRepository;

    // 构造器注入
    public TransactionService(TransactionRepository transactionRepository) {
        this.transactionRepository = transactionRepository;
    }

    public Transaction saveTransaction(Transaction transaction) {
        return transactionRepository.save(transaction);
    }
}

常见报错排查

  • SQLGrammarException:检查数据库字段类型是否为jsonb/json,实体类的@Column定义是否匹配
  • SerializationException:检查Jackson能否序列化你的自定义实体,比如有没有无参构造器、字段是否可访问
  • ClassCastException:检查nullSafeGet里的反序列化类型是否和实体字段类型完全匹配

内容的提问来源于stack exchange,提问作者Josh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:56:15