spring-bootkotlinproject-reactor

Method signature and error handling within Project Reactor


Kind of running around in circles trying to handle a scenario that I might just not be looking at it the "reactive" way of simply need a break to see the bigger picture.

Lets say I want to save a user to the database but first I need to see if a person with that name exists, so:

Repository using ReactiveCrudRepository

interface UserRepository : ReactiveCrudRepository<User, Long> {

    @Query("SELECT * FROM user WHERE name = :name LIMIT 1")
    fun findByName(name: String): Mono<User>

    fun saveUser(user: User): Mono<User>

}

Business code

fun saveUser(user: User): Mono<User> {
  return repository.findByName(user.name)
    .flatMap { existingUser ->
      return Mono.error(UserExistsException())  //A Mono<User> is expected 
    }
    .switchIfEmpty {
      return repository.saveUser(user)
    }
}

The fact that the repository returns Mono.empty() on nulls (that is, if no user with that name is found) seems to limit the way I can handle the case where the user already exists, because I wanted to throw the error so the caller deals with this in his way.

I tried to return a Mono<Optional<User>> on the repository side so I never get an empty but the library does not allow this.

I'm new to reactive in general and I wanted to keep things simple but also within the good practices, so how should I approach this? Is flatMap what I want here? I need to return a User on success and ideally an exception on failures, but I can't simply throw inside flatMap since it asks for a Mono<User>


Solution

  • In Kotlin, unlike Java, return is not used inside lambdas. This is only allowed inside inline lambdas for early return.

    Besides that there are two more issues with your code:

    1. switchIfEmpty accepts a Mono value, but not a lambda.
    2. Mono.error requires a generic argument of T.

    So here is the fixed code:

    fun saveUser(user: User): Mono<User> =
        repository.findByName(user.name)
            .flatMap { existingUser ->
                Mono.error<User>(UserExistsException())
            }.switchIfEmpty(repository.saveUser(user))
    

    If you're working with Kotlin however with Spring, it's recommended to use Coroutines and avoid Project Reactor if possible. To do so we can redefine the UserRepository using CoroutineCrudRepository instead of ReactiveCrudRepository.

    import org.springframework.data.repository.kotlin.CoroutineCrudRepository
    
    interface UserRepository : CoroutineCrudRepository<User, Long> {
        @Query("SELECT * FROM user WHERE name = :name LIMIT 1")
        suspend fun findByName(name: String): User?
    
        suspend fun saveUser(user: User): User
    }
    

    The resulting code also looks much nicer.

    suspend fun saveUser(user: User): User {
        val foundUser = repository.findByName(user.name)
        return if (foundUser == null) repository.saveUser(user)
        else throw UserExistsException()
    }