使用UserType作为父实体ID加载集合时触发ArrayStoreException(PostgreSQL)
问题场景
使用自定义UserType(包括基于hypersistence-utils实现和自行实现的版本)作为实体Foo的ID类型,当Foo关联FooProgress集合并开启@BatchSize时,在PostgreSQL环境下(开启useArrayForMultiValuedParameters=true),加载Foo集合会触发ArrayStoreException。问题在Hibernate 6.6.18.Final和7.0.3.Final版本中均存在。
触发异常的查询语句:
select pe1_0.foo_id,pe1_0.id,pe1_0.name from foo_progress pe1_0 where pe1_0.foo_id = any (?)
核心栈追踪片段:
java.lang.ArrayStoreException: com.lesiak.test.usertypes.entities.FooId at org.hibernate.type.descriptor.java.ArrayJavaType.unwrap(ArrayJavaType.java:275) at org.hibernate.type.descriptor.java.ArrayJavaType.unwrap(ArrayJavaType.java:34) at org.hibernate.type.descriptor.jdbc.ArrayJdbcType.getArray(ArrayJdbcType.java:183) at org.hibernate.dialect.PostgreSQLArrayJdbcType$1.getArray(PostgreSQLArrayJdbcType.java:81)
原因分析
Hibernate使用CollectionBatchLoaderArrayParam处理批量集合加载时,会将实体ID集合转换为JDBC数组参数。但对于自定义UserType的ID类型FooId,Hibernate默认会创建FooId[]数组,而JDBC需要的是FooId unwrap后的String[]数组(因为FooId对应JDBC类型为VARCHAR)。当尝试将unwrap得到的String存入FooId[]数组时,就会抛出类型不匹配的ArrayStoreException。
本质是Hibernate未正确识别自定义ID类型的数组参数需要转换为unwrap后的目标类型数组。
解决方案
方案1:修改自定义UserType,指定数组参数的元素类型
在FooIdUserType中重写getArrayJavaType方法,明确指定数组的元素类型为String,让Hibernate创建String[]数组来存储unwrap后的ID值:
package com.lesiak.test.usertypes.usertypes; import com.lesiak.test.usertypes.entities.FooId; import io.hypersistence.utils.hibernate.type.DescriptorImmutableType; import org.hibernate.HibernateException; import org.hibernate.type.descriptor.java.ArrayJavaType; import org.hibernate.type.descriptor.java.StringJavaType; import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; public class FooIdUserType extends DescriptorImmutableType<FooId, VarcharJdbcType, FooIdTypeDescriptor> { public FooIdUserType() { super(FooId.class, VarcharJdbcType.INSTANCE, FooIdTypeDescriptor.INSTANCE); } public FooId fromStringValue(CharSequence sequence) throws HibernateException { return this.getExpressibleJavaType().wrap(sequence, null); } @Override public org.hibernate.type.descriptor.java.JavaType<?> getArrayJavaType() { // 指定数组元素类型为String,匹配JDBC的VARCHAR数组类型 return ArrayJavaType.from(StringJavaType.INSTANCE, getJdbcType()); } }
方案2:禁用PostgreSQL数组批量参数
如果不想修改代码,可以通过Hibernate配置关闭数组参数的使用,让Hibernate生成IN子句替代ANY(?):
# application.properties hibernate.dialect.postgresql.use_array_for_multivalued_parameters=false
该方案会失去PostgreSQL数组参数的性能优势,适合临时验证或低并发场景。
验证
修改后重新运行测试代码,fooRepository.findAll()可正常加载所有Foo实体及其关联的FooProgress集合,不再抛出ArrayStoreException。
内容的提问来源于stack exchange,提问作者Lesiak

