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

JPA AttributeConverter转换Set到PostgreSQL数组失败问题排查

问题:自定义Set转PostgreSQL数组转换器导致Hibernate SessionFactory构建失败

问题重现

自定义转换器代码

public class SetConverter implements AttributeConverter<Set<String>, String[]> {
    @Override
    public String[] convertToDatabaseColumn(Set<String> attribute) {
        if (!attribute.isEmpty()) {
            return attribute.toArray(new String[0]);
        }
        return new String[0];
    }

    @Override
    public Set<String> convertToEntityAttribute(String[] dbData) {
        if (Objects.nonNull(dbData)) {
            return new HashSet<>(Arrays.asList(dbData));
        }
        return new HashSet<>();
    }
}

实体类代码

@Entity
@Data
@Table(name = "song")
@EntityListeners(AuditingEntityListener.class)
public class Song {

    @Id
    @GeneratedValue
    private UUID id;

    @Convert(converter = SetConverter.class)
    private Set<String> tags;
 
}

错误堆栈

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is java.lang.ClassCastException: class org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl cannot be cast to class org.hibernate.type.BasicPluralType (org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl and org.hibernate.type.BasicPluralType are in unnamed module of loader 'app')
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1234) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.5.jar:6.1.5]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.4.jar:3.2.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.4.jar:3.2.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.4.jar:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.4.jar:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.4.jar:3.2.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.4.jar:3.2.4]
    at com.group.project.ProjectApplication.main(ProjectApplication.java:12) ~[main/:na]
Caused by: jakarta.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is java.lang.ClassCastException: class org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl cannot be cast to class org.hibernate.type.BasicPluralType (org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl and org.hibernate.type.BasicPluralType are in unnamed module of loader 'app')
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:421) ~[spring-orm-6.1.5.jar:6.1.5]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-6.1.5.jar:6.1.5]
    at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366) ~[spring-orm-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1833) ~[spring-beans-6.1.5.jar:6.1.5]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782) ~[spring-beans-6.1.5.jar:6.1.5]
    ... 16 common frames omitted
Caused by: java.lang.ClassCastException: class org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl cannot be cast to class org.hibernate.type.BasicPluralType (org.hibernate.type.internal.CustomMutabilityConvertedBasicTypeImpl and org.hibernate.type.BasicPluralType are in unnamed module of loader 'app')
    at org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl.getCastTypeName(ArrayDdlTypeImpl.java:34) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.getCastTypeName(AbstractSqlAstTranslator.java:6369) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.visitCastTarget(AbstractSqlAstTranslator.java:6316) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.ast.tree.expression.CastTarget.accept(CastTarget.java:75) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.render(AbstractSqlAstTranslator.java:6450) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:224) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:159) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:142) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.function.CastFunction.render(CastFunction.java:76) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.renderCasted(AbstractSqlAstTranslator.java:5275) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsingQuerySelection(SqlAstTranslatorWithMerge.java:156) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsingQuery(SqlAstTranslatorWithMerge.java:145) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsing(SqlAstTranslatorWithMerge.java:121) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeStatement(SqlAstTranslatorWithMerge.java:77) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.SqlAstTranslatorWithMerge.createMergeOperation(SqlAstTranslatorWithMerge.java:43) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.PostgreSQLDialect.usingMerge(PostgreSQLDialect.java:1516) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.dialect.PostgreSQLDialect.createOptionalTableUpdateOperation(PostgreSQLDialect.java:1508) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.sql.model.internal.OptionalTableUpdate.createMutationOperation(OptionalTableUpdate.java:142) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.mutation.AbstractMutationCoordinator.createOperationGroup(AbstractMutationCoordinator.java:87) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.buildStaticUpdateGroup(UpdateCoordinatorStandard.java:1648) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.<init>(UpdateCoordinatorStandard.java:90) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.mutation.MergeCoordinator.<init>(MergeCoordinator.java:23) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.buildMergeCoordinator(AbstractEntityPersister.java:3680) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.doLateInit(AbstractEntityPersister.java:3438) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.postInstantiate(AbstractEntityPersister.java:3712) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl.finishInitialization(MappingMetamodelImpl.java:201) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.internal.SessionFactoryImpl.initializeMappingModel(SessionFactoryImpl.java:364) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:293) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:450) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1507) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final]
    at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75) ~[spring-orm-6.1.5.jar:6.1.5]
    at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390) ~[spring-orm-6.1.5.jar:6.1.5]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-6.1.5.jar:6.1.5]

原因分析

核心问题在于@Convert注解的设计定位:它仅用于单个基本类型之间的转换,而PostgreSQL数组属于复数类型(Plural Type),Hibernate对复数类型有专门的处理逻辑,要求对应的类型实现BasicPluralType接口。

你自定义的转换器会被Hibernate包装为CustomMutabilityConvertedBasicTypeImpl,这个类并没有实现BasicPluralType接口,因此在处理数组类型的SQL语句(比如MERGE操作中的类型转换)时,会触发强制类型转换失败,最终导致SessionFactory构建失败。

解决方案

方案一:使用Hibernate自带的PostgreSQL数组映射(推荐)

无需自定义转换器,直接利用Hibernate提供的数组类型映射,配合PostgreSQL的text[]字段类型即可:

@Entity
@Data
@Table(name = "song")
@EntityListeners(AuditingEntityListener.class)
public class Song {

    @Id
    @GeneratedValue
    private UUID id;

    @Type(org.hibernate.type.StringArrayType.class)
    @Column(columnDefinition = "text[]")
    private Set<String> tags;
 
}

Hibernate会自动处理Set<String>与PostgreSQLtext[]的双向转换,无需额外编码。

方案二:调整转换器目标类型(转为字符串存储)

如果坚持使用@Convert,可以将集合转为分隔符拼接的字符串,存储在普通varchar字段中(属于基本类型转换,符合@Convert的使用场景):

修改后的转换器

public class SetConverter implements AttributeConverter<Set<String>, String> {
    private static final String SEPARATOR = ",";

    @Override
    public String convertToDatabaseColumn(Set<String> attribute) {
        if (attribute == null || attribute.isEmpty()) {
            return null;
        }
        return String.join(SEPARATOR, attribute);
    }

    @Override
    public Set<String> convertToEntityAttribute(String dbData) {
        if (dbData == null || dbData.isEmpty()) {
            return new HashSet<>();
        }
        return new HashSet<>(Arrays.asList(dbData.split(SEPARATOR)));
    }
}

修改后的实体字段

@Convert(converter = SetConverter.class)
@Column(name = "tags")
private Set<String> tags;

注意:如果标签中包含分隔符,需要额外处理转义逻辑。

方案三:注册自定义Hibernate复数类型(高级)

如果需要保留数组存储且自定义转换逻辑,可以实现Hibernate的UserType或CompositeUserType接口,通过@Type注解引用,这种方式能适配Hibernate的复数类型处理逻辑,但实现复杂度较高。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 15:09:51