Spring Boot+Hibernate中Map转PostgreSQL数组出现类型转换异常
问题:Spring Boot + PostgreSQL 中 Map<DayOfWeek, Integer> 映射到 integer[7] 数组报错
环境与需求
- Spring Boot 3.4.4
- PostgreSQL 15
- 需求:将Java的
Map<DayOfWeek, Integer>映射到PostgreSQL的integer[7]数组,数组下标对应DayOfWeek.ordinal(),例如[0,2,3,0,0,0,0]表示TUESDAY对应值2,WEDNESDAY对应值3,其余日期为0。
现有代码
转换器实现
@Converter public class MyConverter implements AttributeConverter<Map<DayOfWeek, Integer>, Integer[]> { @Override public Integer[] convertToDatabaseColumn(Map<DayOfWeek, Integer> map) { return Arrays.stream(DayOfWeek.values()) .map(type -> map.getOrDefault(type, 0)) .toArray(Integer[]::new); } @Override public Map<DayOfWeek, Integer> convertToEntityAttribute(Integer[] values) { return Arrays.stream(DayOfWeek.values()) .collect(Collectors.toMap(type -> type, dayOfWeek -> values[dayOfWeek.ordinal()])); } }
Entity属性定义
@Convert(converter = MyConverter.class) @Column(name = "dow_quantities", nullable = false, columnDefinition = "integer[7]") private Map<DayOfWeek, Integer> daysOfWeekQuantities;
数据库表结构
dow_quantities INTEGER[7] NOT NULL,
抛出异常
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:37) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.getCastTypeName(AbstractSqlAstTranslator.java:7088) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.visitCastTarget(AbstractSqlAstTranslator.java:7035) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.ast.tree.expression.CastTarget.accept(CastTarget.java:80) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.render(AbstractSqlAstTranslator.java:7186) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:233) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:167) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.query.sqm.produce.function.internal.PatternRenderer.render(PatternRenderer.java:150) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.function.CastFunction.render(CastFunction.java:87) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.ast.spi.AbstractSqlAstTranslator.renderCasted(AbstractSqlAstTranslator.java:5823) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsingQuerySelection(SqlAstTranslatorWithMerge.java:161) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsingQuery(SqlAstTranslatorWithMerge.java:150) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeUsing(SqlAstTranslatorWithMerge.java:126) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.SqlAstTranslatorWithMerge.renderMergeStatement(SqlAstTranslatorWithMerge.java:82) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.SqlAstTranslatorWithMerge.createMergeOperation(SqlAstTranslatorWithMerge.java:43) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.PostgreSQLDialect.usingMerge(PostgreSQLDialect.java:1570) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.dialect.PostgreSQLDialect.createOptionalTableUpdateOperation(PostgreSQLDialect.java:1562) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.sql.model.internal.OptionalTableUpdate.createMutationOperation(OptionalTableUpdate.java:144) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.mutation.AbstractMutationCoordinator.createOperationGroup(AbstractMutationCoordinator.java:87) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.buildStaticUpdateGroup(UpdateCoordinatorStandard.java:1663) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.<init>(UpdateCoordinatorStandard.java:93) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.mutation.MergeCoordinator.<init>(MergeCoordinator.java:23) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.AbstractEntityPersister.buildMergeCoordinator(AbstractEntityPersister.java:3827) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.AbstractEntityPersister.doLateInit(AbstractEntityPersister.java:3569) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.persister.entity.AbstractEntityPersister.postInstantiate(AbstractEntityPersister.java:3859) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl.finishInitialization(MappingMetamodelImpl.java:203) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.internal.SessionFactoryImpl.initializeMappingModel(SessionFactoryImpl.java:373) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:302) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:463) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1517) ~[hibernate-core-6.6.11.Final.jar:6.6.11.Final] at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:66) ~[spring-orm-6.2.5.jar:6.2.5] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390) ~[spring-orm-6.2.5.jar:6.2.5] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:419) ~[spring-orm-6.2.5.jar:6.2.5] ... 19 common frames omitted
解决方案
问题根源是Hibernate处理数组类型自定义转换器时,无法正确识别BasicPluralType导致类型转换失败,以下是三种可行解决方式:
方案1:用Hibernate @Type替代JPA @Convert
借助Hibernate提供的数组类型映射,先将属性映射为Integer[],再手动处理与Map<DayOfWeek, Integer>的转换:
- 修改Entity属性定义:
@Type(value = IntegerArrayType.class) @Column(name = "dow_quantities", nullable = false, columnDefinition = "integer[7]") private Integer[] daysOfWeekQuantities;
- 封装转换工具方法:
public Map<DayOfWeek, Integer> arrayToMap(Integer[] array) { return Arrays.stream(DayOfWeek.values()) .collect(Collectors.toMap(day -> day, day -> array[day.ordinal()])); } public Integer[] mapToArray(Map<DayOfWeek, Integer> map) { return Arrays.stream(DayOfWeek.values()) .map(day -> map.getOrDefault(day, 0)) .toArray(Integer[]::new); }
注:需确保依赖中包含对应版本的hibernate-types包(如hibernate-types-60适配Hibernate 6.x)。
方案2:自定义Hibernate UserType
实现Hibernate的UserType接口,直接完成Map<DayOfWeek, Integer>到PostgreSQL数组的映射:
- 自定义类型实现:
public class DayOfWeekMapType implements UserType { private static final int[] SQL_TYPES = {Types.ARRAY}; @Override public int[] sqlTypes() { return SQL_TYPES; } @Override public Class<?> returnedClass() { return Map.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 { Integer[] array = (Integer[]) rs.getArray(names[0]).getArray(); return Arrays.stream(DayOfWeek.values()) .collect(Collectors.toMap(day -> day, day -> array[day.ordinal()])); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { @SuppressWarnings("unchecked") Map<DayOfWeek, Integer> map = (Map<DayOfWeek, Integer>) value; Integer[] array = Arrays.stream(DayOfWeek.values()) .map(day -> map.getOrDefault(day, 0)) .toArray(Integer[]::new); st.setArray(index, session.connection().createArrayOf("integer", array)); } @Override public Object deepCopy(Object value) throws HibernateException { if (value == null) return null; @SuppressWarnings("unchecked") Map<DayOfWeek, Integer> map = (Map<DayOfWeek, Integer>) value; return new EnumMap<>(map); } @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); } }
- 在Entity中使用该类型:
@Type(value = DayOfWeekMapType.class) @Column(name = "dow_quantities", nullable = false, columnDefinition = "integer[7]") private Map<DayOfWeek, Integer> daysOfWeekQuantities;
方案3:禁用PostgreSQL MERGE策略
异常栈显示问题出在MERGE语句处理中,可通过配置强制使用UPDATE语句规避:
在application.properties中添加:
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.hql.bulk_id_strategy=org.hibernate.hql.spi.id.inline.InlineIdsInClauseBulkIdStrategy
说明
方案1、2为长期可靠解决方案,其中方案2可直接在Entity中使用Map<DayOfWeek, Integer>类型,无需手动转换,推荐优先采用;方案3为临时规避手段,仅适用于特定场景。
内容的提问来源于stack exchange,提问作者Juan Diego
相关产品推荐
相关产品推荐

