hibernatejpa

ArrayStoreException when loading collections with UserType in parent entity


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);
    }

}

Solution

  • It is an already reported Hibernate bug: HHH-16991 EnhancedUserType cannot be used when defining relations