Skip to content

Projection .project() method is ignored when using .as() with class-based projections in spring-data-jdbc (and fails conversion) #2098

@VariabileAleatoria

Description

@VariabileAleatoria

Description

When using the fluent query API with findBy(), the .project() method appears to be ignored when combined with .as() for class-based projections. The query executes successfully but fetches all columns from the database instead of only the projected columns, leading to unnecessary data transfer and potential performance issues.
Moreover it fails at converting to the Projection class (this does not happen if using a projection interface)

My setup

Entity

@Data
@NoArgsConstructor
@Table(name = "users")
public class User {
    @Id
    private Long id;
    private String username;
    private String password;
    private String email;
}

Projection class

public record UserPreview(String username) {
}

Repository usage

User user = new User();
user.setUsername("admin");
Example<User> example = Example.of(user);

var result = userRepository.findBy(example, q -> q
    .as(UserPreview.class)
    .project("username")
    .page(Pageable.unpaged()));

 

Error/Issue

The above code produce sql similar to

/*SQL l:167 #:1*/SELECT \"users\".\"ID\" AS \"ID\", \"users\".\"EMAIL\" AS \"EMAIL\", \"users\".\"PASSWORD\" AS \"PASSWORD\", \"users\".\"USERNAME\" AS \"USERNAME\" FROM \"users\" WHERE (\"users\".\"USERNAME\" = ?) {1: 'admin'};

and later crashes with stack trace as

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [it.variabilealeatoria.springdata.demo.User] to type [it.variabilealeatoria.springdata.demo.UserPreview]
        at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:294)
        at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:185)
        at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:165)
        at org.springframework.data.jdbc.repository.support.FluentQuerySupport.lambda$getConversionFunction$1(FluentQuerySupport.java:125)
        at org.springframework.data.jdbc.repository.support.FetchableFluentQueryByExample.lambda$page$2(FetchableFluentQueryByExample.java:125)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:215)
        at java.base/java.util.ArrayList$Itr.forEachRemaining(ArrayList.java:1086)
        at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
        at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
        at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:727)
        at org.springframework.data.domain.Chunk.getConvertedContent(Chunk.java:131)
        at org.springframework.data.domain.PageImpl.map(PageImpl.java:86)
        at org.springframework.data.jdbc.repository.support.FetchableFluentQueryByExample.page(FetchableFluentQueryByExample.java:125)
        at it.variabilealeatoria.springdata.demo.DemoApplicationTests.lambda$0(DemoApplicationTests.java:29)
        at org.springframework.data.jdbc.repository.support.SimpleJdbcRepository.findBy(SimpleJdbcRepository.java:202)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:360)
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
        at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
        at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:515)
        at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:284)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:734)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:174)
        at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
        at jdk.proxy2/jdk.proxy2.$Proxy80.findBy(Unknown Source)
        at it.variabilealeatoria.springdata.demo.DemoApplicationTests.userRepositoryTest(DemoApplicationTests.java:26)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)

Expected behaviour

If the Fluent Api of Query by Example is not specific to spring data JPA it should work also for spring data jdbc.
The same code, switching from spring-data-jdbc to spring-data-jpa works as a charm

The updated User class

@Data
@NoArgsConstructor
@Table(name = "users")
@Entity
public class User {
    @Id
    private Long id;
    private String username;
    private String password;
    private String email;
}

Everyhing else is unmodified

The executed sql is

/*SQL l:58 #:1*/select u1_0.username from users u1_0 where u1_0.username=? {1: 'admin'};

Why is .project ignored in spring-data-jdbc implementation?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions