spring-projects / spring-data-relational

Spring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.
https://spring.io/projects/spring-data-jdbc
Apache License 2.0
758 stars 344 forks source link

Interface projection fails to instantiate data class with non-null constraints #1687

Closed w3-3w closed 9 months ago

w3-3w commented 9 months ago

TL;DR: When using combination of Kotlin and R2dbc, interface-based and class-based projection does not work as described in documentation.

Spring Data Version: 3.2.0

Example:

@Table("user")
@Immutable
data class UserEntity(
    @Id
    val id: Long = 0L,
    val name: String,
    val loginId: String,
)

interface UserProjection {
    val id: Long
    val name: String
}

data class UserDto(
    val id: Long,
    val name: String,
)

interface UserRepository : CoroutineCrudRepository<UserEntity, Long> {
    suspend fun findByLoginId(loginId: String): List<UserDto> // (1) this causes startup failure
    suspend fun getByLoginId(loginId: String): List<UserProjection> // (2) this throws runtime exception
}

For (1), the whole application fails to start with exception:

Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.lang.Object my.package.UserRepository.findByLoginId(java.lang.String,kotlin.coroutines.Continuation); Reason: Failed to create query for method public abstract java.lang.Object my.package.UserRepository.findByLoginId(java.lang.String,kotlin.coroutines.Continuation); No property 'loginId' found for type 'UserDto'
    at org.springframework.data.repository.query.QueryCreationException.create(QueryCreationException.java:101)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:115)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.mapMethodsToQuery(QueryExecutorMethodInterceptor.java:99)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lambda$new$0(QueryExecutorMethodInterceptor.java:88)
    at java.base/java.util.Optional.map(Optional.java:260)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.<init>(QueryExecutorMethodInterceptor.java:88)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:357)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:279)
    at org.springframework.data.util.Lazy.getNullable(Lazy.java:135)
    at org.springframework.data.util.Lazy.get(Lazy.java:113)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:285)
    at org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean.afterPropertiesSet(R2dbcRepositoryFactoryBean.java:159)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1822)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1771)
    ... 30 common frames omitted
Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.lang.Object my.package.UserRepository.findByLoginId(java.lang.String,kotlin.coroutines.Continuation); No property 'loginId' found for type 'UserDto'
    at org.springframework.data.r2dbc.repository.query.PartTreeR2dbcQuery.<init>(PartTreeR2dbcQuery.java:74)
    at org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory$R2dbcQueryLookupStrategy.resolveQuery(R2dbcRepositoryFactory.java:179)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:111)
    ... 42 common frames omitted
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'loginId' found for type 'UserDto'
    at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:90)
    at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:443)
    at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:419)
    at org.springframework.data.mapping.PropertyPath.lambda$from$0(PropertyPath.java:372)
    at java.base/java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:330)
    at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:354)
    at org.springframework.data.mapping.PropertyPath.from(PropertyPath.java:332)
    at org.springframework.data.repository.query.parser.Part.<init>(Part.java:81)
    at org.springframework.data.repository.query.parser.PartTree$OrPart.lambda$new$0(PartTree.java:259)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
    at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at org.springframework.data.repository.query.parser.PartTree$OrPart.<init>(PartTree.java:260)
    at org.springframework.data.repository.query.parser.PartTree$Predicate.lambda$new$0(PartTree.java:389)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
    at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at org.springframework.data.repository.query.parser.PartTree$Predicate.<init>(PartTree.java:390)
    at org.springframework.data.repository.query.parser.PartTree.<init>(PartTree.java:103)
    at org.springframework.data.r2dbc.repository.query.PartTreeR2dbcQuery.<init>(PartTreeR2dbcQuery.java:70)
        ... 44 common frames omitted

For (2), exception is like:

Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method my.package.UserEntity.<init>, parameter loginId
    at my.package.UserEntity.<init>(TestUserRepository.kt)
    at my.package.UserEntity.<init>(TestUserRepository.kt:10)
    at my.package.UserEntity_Instantiator_n2pr7c.newInstance(Unknown Source)
    at org.springframework.data.mapping.model.KotlinClassGeneratingEntityInstantiator$DefaultingKotlinClassInstantiatorAdapter.createInstance(KotlinClassGeneratingEntityInstantiator.java:100)
    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.asyncer.r2dbc.mysql.MySqlResult.lambda$map$1(MySqlResult.java:87)
mp911de commented 9 months ago

These are actually two issues, I created #1688 to track the problem with properties not existent in the projection type which is an actual bug.

Up to now, we instantiate the underlying entity type with a smaller set of properties to then apply a projection on top. Due to class design restrictions, especially Kotlin's non-null by default, we should use a different mechanism that doesn't require entity instantiation.