spring-projects / spring-data-jpa

Simplifies the development of creating a JPA-based data access layer.
https://spring.io/projects/spring-data-jpa/
Apache License 2.0
3.03k stars 1.42k forks source link

Interface-based projection does not work for queries with declared fields when returned interface is implemented by the entity #3537

Closed vitos23 closed 4 months ago

vitos23 commented 4 months ago

Interface-based projection does not work properly when querying for not the whole entity but specified fields and returned interface is implemented by the entity.

Suppose we have:

interface UserInfo {
    val name: String
    val age: Int
}

@Entity
@Table(name = "users")
class User(
    @Column(name = "name")
    override var name: String,
    @Column(name = "age")
    override var age: Int,
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    var id: Long? = null,
) : UserInfo

interface UserRepository : JpaRepository<User, String> {

    @Query("select name, age from users", nativeQuery = true)
    fun findAllInfos(): List<UserInfo>

}

Let's put some data in the users table: userRepository.save(User("user", 123)). And if we execute findAllInfos we will get the following error:

org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.Object[]] to type [@org.springframework.data.jpa.repository.Query com.example.projectiondemo.UserInfo] for value [{...}]
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
...
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [@org.springframework.data.jpa.repository.Query com.example.projectiondemo.UserInfo]
...

The full example is here.

The root of the problem is on the 139th line of the ResultProcessor class (spring-data-commons), in the method processResult:

if (source == null || type.isInstance(source) || !type.isProjecting()) {
    return (T) source;
}

type.isProjecting() returns false but source is just Object with queried fields, not the entity. Therefore, projection is not created.

odrotbohm commented 4 months ago

Projection interfaces must not be implemented by the aggregate which the repository manages. If it implements the interface we cannot distinguish between a lookup for the full entity behind the interface or just the reduced set of properties. A simple workaround would be to create an additional, empty projection interface that extends UserInfo to make clear what you're tying to do.

vitos23 commented 4 months ago

Thanks for the workaround. It would be nice to see such limitations covered in the documentation.