micronaut-projects / micronaut-data

Ahead of Time Data Repositories
Apache License 2.0
465 stars 197 forks source link

When attempting to query multiple entities with Micronaut Data & Hibernate, Typedef not being honoured for Unwrap strategy #2797

Closed rorychatterton closed 7 months ago

rorychatterton commented 8 months ago

When using a custom type annotated with Typedef, JPA Hibernate Repository queries that return more than one object throws Unwrap strategy not known for this Java type errors.

This error does not appear to occur with get/save/update/delete.

Expected Behavior

When running queries like repository.findAll(), custom types should be properly deserialised from the database using the Micronaut Data Attribute Converter.

Actual Behaviour

Error message when running repository.findAll():

java.lang.UnsupportedOperationException: Unwrap strategy not known for this Java type : io.ozee.WrapperID
    at org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType.unwrap(UnknownBasicJavaType.java:60)
    at org.hibernate.type.descriptor.jdbc.UUIDJdbcType$1.doBind(UUIDJdbcType.java:65)
    at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
    at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.lambda$beforeStatement$0(JdbcValueBindingsImpl.java:87)
    at java.base/java.lang.Iterable.forEach(Iterable.java:75)
    at org.hibernate.engine.jdbc.mutation.spi.BindingGroup.forEachBinding(BindingGroup.java:51)
    at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.beforeStatement(JdbcValueBindingsImpl.java:85)
    at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:104)
    at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:40)
    at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:52)
    at org.hibernate.persister.entity.mutation.InsertCoordinator.doStaticInserts(InsertCoordinator.java:175)
    at org.hibernate.persister.entity.mutation.InsertCoordinator.coordinateInsert(InsertCoordinator.java:113)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2873)
    at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:104)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:632)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:499)
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:363)
    at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:61)
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
    at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1366)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$2(ConcreteSqmSelectQueryPlan.java:136)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:362)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:303)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:509)
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:427)
    at org.hibernate.query.Query.getResultList(Query.java:120)
    at io.micronaut.data.hibernate.operations.HibernateJpaOperations$ListResultCollector.collect(HibernateJpaOperations.java:669)
    at io.micronaut.data.hibernate.operations.HibernateJpaOperations$ListResultCollector.collect(HibernateJpaOperations.java:658)
    at io.micronaut.data.hibernate.operations.AbstractHibernateOperations.collectResults(AbstractHibernateOperations.java:405)
    at io.micronaut.data.hibernate.operations.AbstractHibernateOperations.collectFindAll(AbstractHibernateOperations.java:346)
    at io.micronaut.data.hibernate.operations.HibernateJpaOperations.lambda$findAll$8(HibernateJpaOperations.java:318)
    at io.micronaut.data.hibernate.operations.HibernateJpaOperations.lambda$executeRead$21(HibernateJpaOperations.java:548)

Steps To Reproduce

Please find this repository to reproduce the error.

  1. Clone it,
  2. ./gradlew test

Screenshot 2024-02-16 at 12 03 14 pm

Entity:

@Entity
class TestEntity {
    @Id
    @JdbcTypeCode(SqlTypes.UUID)
    var id: WrapperID = WrapperID()

    ...
}

WrapperID

@TypeDef(type = DataType.UUID, converter = WrapperIDConverter::class)
data class WrapperID(
    private val id: UUID,
    private var prefix: String = PLACEHOLDER_PREFIX
)

Converter

@Singleton
open class WrapperIDConverter : AttributeConverter<WrapperID, UUID> {
    override fun convertToPersistedValue(entityValue: WrapperID?, context: ConversionContext?): UUID? {
        return entityValue?.toUUID()
    }

    override fun convertToEntityValue(persistedValue: UUID?, context: ConversionContext?): WrapperID? {
        return persistedValue?.let { WrapperID(it) }
    }
}

Environment Information

Example Application

https://github.com/rorychatterton/micronaut_data_error_reproduction

Version

4.3.1

rorychatterton commented 8 months ago

Adding a Custom Hibernate JavaType fixed it.

This is unexpected, as it isn't mentioned in the Micronaut Data Docs for custom types. Should Micronauts Typedef annotation and AOT Compilation wrap and apply these annotations / add this code on behalf of the customer when using Micronaut Data w' Hibernate?

Edit: On re-review of the documentation, it appears that the TypeDef is only guaranteed to work within the context of Micronaut Data JDBC and provides no guarantees to work with JPA more broadly. My issues seem to be as a result of proxying and other features that aren't supported by Micronaut Data JDBC/R2JDBC.

Entity

    @Id
    @JdbcTypeCode(SqlTypes.UUID) 
    @JavaType(WrapperIDDataType::class) // <- This
    var id: WrapperID = WrapperID()

Type

class WrapperIDDataType: AbstractClassJavaType<WrapperID>(WrapperID::class.java) {
    override fun <X : Any?> unwrap(value: WrapperID?, clazz: Class<X>?, options: WrapperOptions?): X {
        return UUIDJavaType.INSTANCE.unwrap(value?.toUUID(), clazz, options)
    }

    override fun <X : Any?> wrap(value: X?, options: WrapperOptions?): WrapperID? {
        return value
            ?.let { UUIDJavaType.INSTANCE.wrap(value, options) }
            ?.let { WrapperID(it, "PREFIX") }
    }
}