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.02k stars 1.42k forks source link

`suspending` support within `JpaRepository` #3598

Closed cmdjulian closed 2 months ago

cmdjulian commented 3 months ago

This is a small extract from my demo project from GitHub including some tests:

@RestController
class HelloController(private val service: HelloService) {
    @PutMapping("/hello/{id}")
    suspend fun update(@PathVariable("id") id: Long): Unit = service.update(id)

    @GetMapping("/hello/{id}")
    suspend fun find(@PathVariable("id") id: Long): HelloEntity? = service.find(id)
}

@Service
class HelloService(private val helloRepository: HelloRepository) {
    @Transactional
    suspend fun update(id: Long) {
        helloRepository.updateMessage(id, "Hello, Spring Boot!")
    }

    @Transactional
    suspend fun find(id: Long): HelloEntity? = helloRepository.suspendFindById(id)
}

@Entity
class HelloEntity(@Id var id: Long? = null, var message: String = "Hello")

@Repository
interface HelloRepository : ListCrudRepository<HelloEntity, Long> {
    @Modifying
    @Query("update HelloEntity e set e.message = :message where e.id = :id")
    suspend fun updateMessage(id: Long, message: String): Int

    @Query("select e from HelloEntity e where e.id = :id")
    suspend fun suspendFindById(id: Long): HelloEntity?

    @Query("select e from HelloEntity e")
    fun findAllFlow(): Flow<HelloEntity>
}

I found multiple problems:

At the moment suspend functions are pretty unusable at all from my app. Is this something which should be supported? Would definitely appreciate it. The only thing I found is within the Spring Data Commons Reference Documentation, which seems to outline support.

Some more background information: We use a lot of suspending functions in our Kotlin Spring Boot app which get invoked within the controller. At the moment we use blocking JPA repository methods and wrap them manually in a withContext(Dispatcher.IO) call, which is not just tedious, but also error prone.

Being able to natively use them within the repository would help us in reducing complexity here.

For instance, JooQ also supports Kotlin Coroutines even for blocking JDBC drivers.

Other considered resources:

mp911de commented 2 months ago

Coroutine repositories are built on top of Reactive repositories. With JPA not supporting a reactive API (note: not talking about Hibernate specifically), we do not have any means to implement reactive JPA in the first place.

Reactive flows do not (really) care about threading, and so does Kotlin Coroutines not care either.

Regarding jooq:

Starting from jOOQ 3.17, the jooq-kotlin-coroutines extension module allows for bridging between the reactive streams API and the coroutine APIs.

So the actual magic is that jooq uses R2DBC drivers that have a different sense of context propagation. Also, jooq ties itself to a connection by encapsulating exactly that one resource.

Spring's Transaction Management requires either imperative (your code stays on the same thread) or reactive (reactive context propagation) code styles but you cannot mix these because of how context is propagated.

Offloading calls to a dedicated executor that maintains transactional scopes, regardless of whether the executor uses Virtual Threads or utilities from Kotlin Coroutines, is likely the only good workaround.

Going forward, we do not plan on enhancing asynchronous JPA capabilities. In some sense, Kotlin created that problem space; It would be great if Kotlin had an answer on how to integrate existing non-Coroutine code in a similar way that Project Loom brought into the JVM instead of requiring all other parts of the ecosystem to follow Kotlin rules.