Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
w3-3w opened this issue Dec 5, 2023 · 1 comment
Closed
Assignees
Labels
type: enhancement A general enhancement

Comments

@w3-3w
Copy link

w3-3w commented Dec 5, 2023

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)
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 5, 2023
@mp911de mp911de added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 5, 2023
@mp911de mp911de self-assigned this Dec 5, 2023
@mp911de mp911de changed the title Projection failure when using r2dbc with entities in Kotlin with non-null fields Interface projection fails to instantiate data class with non-null constraints Dec 5, 2023
@mp911de mp911de added type: enhancement A general enhancement and removed type: bug A general bug labels Dec 5, 2023
@mp911de
Copy link
Member

mp911de commented Dec 5, 2023

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.

@mp911de mp911de added this to the 3.2.1 (2023.1.1) milestone Dec 5, 2023
@mp911de mp911de closed this as completed in 61a1451 Dec 5, 2023
mp911de added a commit that referenced this issue Dec 5, 2023
Previously, we instantiated the underlying entity. Now, we either read results directly into the result type or use a Map-backed projection.

Closes #1687
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants