I am struggling to use a UserType as a key of my parent entity.
The error manifests itself when the parent entity has a Collection.
The collection has to be loaded with CollectionBatchLoaderArrayParam (and thus it only happens on Dialects with useArrayForMultiValuedParameters=true
such as Postgres).
I am using hypersistence-utils to define my user type, but I checked that this is an irrelevant detail - the same happens with UserType implemented from scratch)
Is there anything wrong about my user type or its usage (Note: I know I can use @EmbeddedId instead, but let's concentrate on User Types) or is it a bug in Hibernate.
Hibernate versions checked: 6.6.18.Final and 7.0.3.Final
Generated Query loading the collection:
select pe1_0.foo_id,pe1_0.id,pe1_0.name from foo_progress pe1_0 where pe1_0.foo_id = any (?)
Stack trace:
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.access$100(PostgreSQLArrayJdbcType.java:28)
at org.hibernate.dialect.PostgreSQLArrayJdbcType$1.getArray(PostgreSQLArrayJdbcType.java:81)
at org.hibernate.dialect.PostgreSQLArrayJdbcType$1.doBind(PostgreSQLArrayJdbcType.java:42)
at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
at org.hibernate.sql.exec.internal.AbstractJdbcParameter.bindParameterValue(AbstractJdbcParameter.java:113)
at org.hibernate.sql.exec.internal.AbstractJdbcParameter.bindParameterValue(AbstractJdbcParameter.java:84)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.bindParameters(DeferredResultSetAccess.java:207)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:237)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:171)
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.<init>(JdbcValuesResultSetImpl.java:74)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValuesSource(JdbcSelectExecutorStandardImpl.java:355)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:137)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:142)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:132)
at org.hibernate.loader.ast.internal.CollectionBatchLoaderArrayParam.initializeKeys(CollectionBatchLoaderArrayParam.java:204)
at org.hibernate.loader.ast.internal.AbstractCollectionBatchLoader.load(AbstractCollectionBatchLoader.java:94)
at org.hibernate.loader.ast.internal.CollectionBatchLoaderArrayParam.load(CollectionBatchLoaderArrayParam.java:118)
at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:748)
at org.hibernate.event.internal.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:69)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:1724)
at org.hibernate.collection.spi.AbstractPersistentCollection.lambda$initialize$3(AbstractPersistentCollection.java:616)
at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:265)
at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:614)
at org.hibernate.collection.spi.AbstractPersistentCollection.forceInitialization(AbstractPersistentCollection.java:817)
at org.hibernate.engine.internal.StatefulPersistenceContext.initializeNonLazyCollections(StatefulPersistenceContext.java:1169)
at org.hibernate.engine.internal.StatefulPersistenceContext.initializeNonLazyCollections(StatefulPersistenceContext.java:1155)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:224)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:35)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:224)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$1(ConcreteSqmSelectQueryPlan.java:152)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:442)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:362)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:380)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:143)
at org.hibernate.query.Query.getResultList(Query.java:120)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:393)
Example code:
Key
@Data
@AllArgsConstructor
public class FooId implements Serializable {
private static final long serialVersionUID = 1L;
private final String id;
@Override
public String toString() {
return id;
}
}
Parent Entity
@Entity
@Table(name = "foos")
@Data
@NoArgsConstructor
public class Foo {
@Id
@Type(FooIdUserType.class) //NOTE the usage of User Type
private FooId fooId;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "foo", fetch = EAGER, orphanRemoval = true, cascade = ALL)
@BatchSize(size = 10)
private Set<FooProgress> progressEntities = new HashSet<>();
public Foo(FooId fooId, String name) {
this.fooId = fooId;
this.name = name;
}
public void addProgressEntity(FooProgress progress) {
this.progressEntities.add(progress);
progress.setFoo(this); // Ensure bidirectional relationship is maintained
}
}
Child Entity:
@Entity
@Table(name = "foo_progress")
@Data
@EqualsAndHashCode(exclude = "foo")
@ToString(exclude = "foo")
@NoArgsConstructor
@AllArgsConstructor
public class FooProgress {
@Id
private String id;
private String name;
@ManyToOne
@JoinColumn(name = "foo_id")
private Foo foo;
}
User Type:
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.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);
}
}
public class FooIdTypeDescriptor extends AbstractClassJavaType<FooId> {
public static final FooIdTypeDescriptor INSTANCE = new FooIdTypeDescriptor();
public FooIdTypeDescriptor() {
super(FooId.class);
}
public <X> X unwrap(FooId value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
} else if (String.class.isAssignableFrom(type)) {
return (X)value.getId();
} else {
throw this.unknownUnwrap(type);
}
}
public <X> FooId wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
} else if (value instanceof String) {
return new FooId((String)value);
} else {
throw this.unknownWrap(value.getClass());
}
}
}
Test code:
@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(classes = {EmbeddedPostgresConfiguration.class})
public class FooRepositoryTest {
@Autowired
private FooRepository fooRepository;
@Test
@Transactional
void shouldLoadAllEntities() throws Exception {
var fooId1 = new FooId("1");
var fooId2 = new FooId("2");
var fooProgress1 = new FooProgress("p1", "foo1Progress", null);
var fooProgress2 = new FooProgress("p2", "foo2Progress", null);
var foo1 = new Foo(fooId1, "foo1");
var foo2 = new Foo(fooId2, "foo2");
foo1.addProgressEntity(fooProgress1);
foo1.addProgressEntity(fooProgress2);
fooRepository.save(foo1);
fooRepository.save(foo2);
// Commit the first transaction
TestTransaction.flagForCommit(); // Mark the current test transaction for commit
TestTransaction.end();
TestTransaction.start();
List<Foo> all = fooRepository.findAll(); // Fails here
all.forEach(System.out::println);
}
}
It is an already reported Hibernate bug: HHH-16991 EnhancedUserType cannot be used when defining relations