spring-data-relational icon indicating copy to clipboard operation
spring-data-relational copied to clipboard

Embedded entities can not be constructed when they use Kotlin `val` properties

Open koenpunt opened this issue 1 year ago • 6 comments

To retrieve results indexed by a specific column (relation_id), we use "pair" classes. For example;

// Repository method
@Query(
    """
        SELECT o.id AS relation_id, c.* FROM customer_details c
        JOIN orders o ON o.customer_details_id = c.id
        WHERE o.id IN (:ids)
    """
)
suspend fun findAllByOrderIdIn(ids: Iterable<UUID>): List<CustomerDetailsPair>

// ...

// The virtual pair entity
data class CustomerDetailsPair(
    override val relationId: UUID,
    @Embedded.Empty
    override val entity: CustomerDetails,
)

// The CustomerDetails entity
@Table(name = "customer_details")
class CustomerDetails(
    val name: String,
    var email: String?,
)

But this results in the following exception being thrown:

Cannot set property name because no setter, no wither and it's not part of the persistence constructor public oopen.timemachine.entities.CustomerDetails(java.lang.String,java.lang.String)

When changing the name property to a var, no error is thrown. The entity as a standalone entity, so not embedded, can be constructed without changing the property to a var.

koenpunt avatar Nov 27 '23 13:11 koenpunt

Care to include the full stack trace?

mp911de avatar Nov 27 '23 13:11 mp911de

Here you go:

Caused by: java.lang.IllegalStateException: Cannot set property name because no setter, no wither and it's not part of the persistence constructor public oopen.timemachine.entities.CustomerDetails(java.lang.String,java.lang.String,com.google.i18n.phonenumbers.Phonenumber$PhoneNumber,java.util.UUID,boolean,java.util.UUID,boolean)
	at org.springframework.data.mapping.model.InstantiationAwarePropertyAccessor.setProperty(InstantiationAwarePropertyAccessor.java:93)
	at org.springframework.data.mapping.model.ConvertingPropertyAccessor.setProperty(ConvertingPropertyAccessor.java:60)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readProperties(MappingRelationalConverter.java:556)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.populateProperties(MappingRelationalConverter.java:523)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:457)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readEmbedded(MappingRelationalConverter.java:566)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter$2.getPropertyValue(MappingRelationalConverter.java:490)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter$2.getPropertyValue(MappingRelationalConverter.java:475)
	at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:71)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter$ConvertingParameterValueProvider.getParameterValue(MappingRelationalConverter.java:1161)
	at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:49)
	at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.extractInvocationArguments(ClassGeneratingEntityInstantiator.java:301)
	at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:273)
	at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:98)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:454)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:348)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:311)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:298)
	at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:294)
	at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$getRowsFetchSpec$13(R2dbcEntityTemplate.java:795)
	at io.r2dbc.proxy.callback.ResultCallbackHandler.lambda$invoke$0(ResultCallbackHandler.java:92)
	at io.r2dbc.proxy.callback.ResultCallbackHandler.lambda$invoke$0(ResultCallbackHandler.java:92)
	at io.r2dbc.postgresql.PostgresqlResult.lambda$map$2(PostgresqlResult.java:129)
	at reactor.core.publisher.ContextPropagation.lambda$contextRestoreForHandle$2(ContextPropagation.java:173)
	at reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:113)
	at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:250)
	at reactor.core.publisher.FluxHandleFuseable$HandleFuseableSubscriber.onNext(FluxHandleFuseable.java:194)
	at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at io.r2dbc.postgresql.util.FluxDiscardOnCancel$FluxDiscardOnCancelSubscriber.onNext(FluxDiscardOnCancel.java:91)
	at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:113)
	at reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:129)
	at reactor.core.publisher.FluxCreate$BufferAsyncSink.drain(FluxCreate.java:880)
	at reactor.core.publisher.FluxCreate$BufferAsyncSink.next(FluxCreate.java:805)
	at reactor.core.publisher.FluxCreate$SerializedFluxSink.next(FluxCreate.java:163)
	at io.r2dbc.postgresql.client.ReactorNettyClient$Conversation.emit(ReactorNettyClient.java:684)
	at io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.emit(ReactorNettyClient.java:936)
	at io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:810)
	at io.r2dbc.postgresql.client.ReactorNettyClient$BackendMessageSubscriber.onNext(ReactorNettyClient.java:716)
	at reactor.core.publisher.FluxHandle$HandleSubscriber.onNext(FluxHandle.java:129)
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onNext(FluxPeekFuseable.java:854)
	at reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at reactor.core.publisher.FluxMap$MapConditionalSubscriber.onNext(FluxMap.java:224)
	at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:294)
	at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:403)
	at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:426)
	at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:333)
	at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:454)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1623)

koenpunt avatar Nov 27 '23 13:11 koenpunt

Just found out that it doesn't throw an exception when making the CustomerDetails entity a data class, but we're not sure what side effects that could have.

koenpunt avatar Nov 27 '23 13:11 koenpunt

CustomerDetailsPair uses overridden properties. That's not supported as Kotlin creates two properties without being able to set reliably both.

Without the override keyword, I'm not able to reproduce the issue.

mp911de avatar Nov 27 '23 13:11 mp911de

For completeness, the pair class implements an interface, and doesn't inherit from a class;

data class CustomerDetailsPair(
    override val relationId: UUID,
    @Embedded.Empty
    override val entity: CustomerDetails,
) : EntityPair<UUID, CustomerDetails>

interface EntityPair<F, S> {
    val relationId: F
    val entity: S
}

But also when I remove the interface and the override from the properties of the pair the exception is thrown.

koenpunt avatar Nov 27 '23 14:11 koenpunt

@mp911de I noticed you assigned yourself; do you maybe have an update regarding this?

koenpunt avatar Jan 15 '24 09:01 koenpunt