Skip to content

Type Constraint Mismatch in QueryByExampleExecutor.findBy() #3986

@lehoanggiap

Description

@lehoanggiap

Summary

After upgrading from Spring Data JPA 4.0.0-M4 to 4.0.0-M5 (Spring Boot 4.0.0-M1 → 4.0.0-M2), custom JPA repository implementations fail to compile due to a type constraint mismatch between the QueryByExampleExecutor.findBy() interface method and the SimpleJpaRepository implementation.

Environment

Spring Boot: 4.0.0-M2 (upgraded from 4.0.0-M1)
Spring Data JPA: 4.0.0-M5 (upgraded from 4.0.0-M4)
Kotlin: 2.2.0
JVM Target: 24
Build Tool: Gradle 9.0.0
OS: macOS Sonoma 14.5
IDE: IntelliJ IDEA Ultimate

Problem Description

The QueryByExampleExecutor.findBy() method signature was updated to support nullable return types:
New Interface Signature (4.0.0-M5):

<S extends T, R extends @Nullable Object> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);

Previous Interface Signature (4.0.0-M4):

<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);

However, SimpleJpaRepository was not updated to match this new signature, creating a type constraint conflict:
Interface allows: R extends @Nullable Object (nullable types)
SimpleJpaRepository requires: R extends Object (non-nullable types only)

Compilation Error

Image

Impact

This affects any custom repository implementation that extends SimpleJpaRepository and implements JpaRepository (which inherits from QueryByExampleExecutor). In our case, this is our base repository class used throughout the project.

Minimal Reproduction Example

package com.respiroc.user.application.payload

import org.springframework.data.domain.Example
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.support.JpaEntityInformation
import org.springframework.data.jpa.repository.support.SimpleJpaRepository
import org.springframework.data.repository.NoRepositoryBean
import org.springframework.data.repository.query.FluentQuery
import jakarta.persistence.EntityManager
import jakarta.persistence.*
import java.util.function.Function

// Simple entity for demonstration
@Entity
data class TestEntity(
    @Id @GeneratedValue
    val id: Long = 0,
    val name: String = ""
)

// Custom repository interface extending JpaRepository
@NoRepositoryBean
interface CustomRepository<T: Any, ID: Any> : JpaRepository<T, ID> {
    // Any custom method
    fun customMethod(): String
}

// Custom repository implementation - THIS FAILS TO COMPILE
class CustomRepositoryImpl<T : Any, ID: Any>(
    entityInformation: JpaEntityInformation<T, ID>,
    entityManager: EntityManager
) : SimpleJpaRepository<T, ID>(entityInformation, entityManager), CustomRepository<T, ID> {

    override fun customMethod(): String = "custom implementation"
    
    // COMPILATION ERROR HERE:
    // Class 'CustomRepositoryImpl' is not abstract and does not implement abstract member:
    // fun <S : T, R> findBy(example: Example<S>, queryFunction: Function<FluentQuery.FetchableFluentQuery<S>, R>): R
}

// Usage example
interface TestEntityRepository : CustomRepository<TestEntity, Long>

Error occurs when:

  1. Creating a custom repository implementation that extends SimpleJpaRepository
  2. The custom repository interface extends JpaRepository (which inherits QueryByExampleExecutor)
  3. Compiling with Spring Data JPA 4.0.0-M5

Current Workaround

We've implemented the following workaround to resolve the compilation issue:

@Suppress("ACCIDENTAL_OVERRIDE", "UNCHECKED_CAST")
override fun <S : T, R: Any?> findBy(
    example: Example<S>,
    queryFunction: Function<FluentQuery.FetchableFluentQuery<S>, R>
): R {
    return super.findBy(example, queryFunction as Function<FluentQuery.FetchableFluentQuery<S>, Any>) as R
}

Workaround Explanation:

  1. Type Casting: Cast queryFunction to match SimpleJpaRepository's non-nullable requirement - The queryFunction passed to this override method expects R: Any? but super.findBy() requires R: Any. At runtime they are the same, but we need to cast to pass the compile phase and bridge the nullable/non-nullable type gap.
  2. @Suppress("UNCHECKED_CAST"): After casting to Any, we cast the result back to R. The compiler cannot verify this type safety, so we suppress the warning since we know the cast is safe.
  3. @Suppress("ACCIDENTAL_OVERRIDE"): Both override signatures fun <S : T, R: Any?> and fun <S : T, R: Any> have identical JVM signatures due to type erasure. This creates a conflict, so we suppress the accidental override warning to resolve the compilation error.
    Testing Results: This workaround has been tested extensively in our application and works correctly with all findBy operations (.all(), .count(), .exists(), .first(), .one()).

Proposed Solution

Update SimpleJpaRepository.findBy() method signature to match the interface:

// Current SimpleJpaRepository signature
public <S extends T, R extends Object> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)

// Should be updated to
public <S extends T, R extends @Nullable Object> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)

Questions

  1. Is this a bug or intentional breaking change?
  2. Will SimpleJpaRepository be updated to align with the interface signature?
  3. Is our workaround approach safe for production use?
  4. Timeline: When can we expect a proper fix?

Additional Notes

  • This issue appears to be Kotlin-specific due to stricter type checking
  • The workaround maintains full functionality while bridging the type constraint gap
  • No runtime issues have been observed with the workaround solution

Metadata

Metadata

Assignees

Labels

has: ai-slopAn bloated issue that contains low-value AI-generated content.type: regressionA regression from a previous release

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions