I am kind of frustrated right now as I thought this will be much easier and the issue would be documented way better but I just can not find a solution. Therefore I seek help here.
I am working on a Kotlin project which leverages spring boot version 2.5.3 and uses spring data jpa for database access and schema definition. Pretty common and straight-forward. Now assume we have some kind of UserService which contains a method updateUsername which gets a username as a parameter und updates the username after it verified its validity by an external service. For demo purposes of the issue I want to highlight, before we verify the username we set the username manually to "foo". This whole unit of work should happend in a transaction which is why the method is annotated with @Transactional. But because of the call to the external service the method will suspend when we wait for the http response (note the suspend keyword on both methods).
@Service
class UserService(private val userRepository: UserRepository) {
@Transactional
suspend fun setUsername(id: UUID, username: String): Person {
logger.info { "Updating username..." }
val user = userRepository.findByIdentityId(identityId = id)
?: throw IllegalArgumentException("User does not exist!")
// we update the username here but the change will be overridden after the verification to the actual username!
user.userName = "foo"
verifyUsername(username)
user.userName = username
return userRepository.save(user)
}
private suspend fun verifyUsername(username: String) {
// assume we are doing some kind of network call here which will suspend while waiting got a response
logger.info { "Verifying..." }
delay(1000)
logger.info { "Finished verifying!" }
}
}
This compiles successfully and I can also execute the method and it starts a new transaction, but the transaction will be commited as soon as we suspend the invocation of the verifyUsername method on calling delay(1000). Therefore our database will acually hold the value "foo" as the username until it is overwritten. But if the code after verifyUsername would fail and throw an exception, we could not rollback this change as the transaction was already commited and foo would stay in the database forever!!! This is definitely not the expected behavior as we only want to commit the transaction at the very end of our method, so we can rollback the transaction at any time if something went wrong. Here you can see the logs:
DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [x.x.x.UserService.setUsername]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(1406394125<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@9ec4d42]
INFO x.x.x.UserService - Updating username...
DEBUG org.hibernate.SQL - select person0_.id as id1_6_, person0_.email as email2_6_, person0_.family_name as family_n3_6_, person0_.given_name as given_na4_6_, person0_.identity_id as identity5_6_, person0_.user_name as user_nam6_6_ from person person0_ where person0_.identity_id=?
INFO x.x.x.UserService - Verifying...
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1406394125<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(1406394125<open>)] after transaction
INFO x.x.x.UserService - Finished verifying
DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(319912425<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1dd261da]
DEBUG org.hibernate.SQL - select person0_.id as id1_6_0_, person0_.email as email2_6_0_, person0_.family_name as family_n3_6_0_, person0_.given_name as given_na4_6_0_, person0_.identity_id as identity5_6_0_, person0_.user_name as user_nam6_6_0_ from person person0_ where person0_.id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(319912425<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(319912425<open>)] after transaction
In this spring article it says that "Transactions on Coroutines are supported via the programmatic variant of the Reactive transaction management provided as of Spring Framework 5.2. For suspending functions, a TransactionalOperator.executeAndAwait extension is provided."
Does that mean that @Transactional just cannot be used on suspending methods and you should programatically handle transaction management? Update: This thread states that @Transactional should work for suspend functions.
I am also aware, that all my database operations are running synchronously, and I could use r2dbc to make them asynchronously (as long as my database provides the driver implementing the specification), but I think that my question does not relate to how I communicate with the database but more about how spring handles suspending calls withing @Transactional annotated methods.
What are your thoughts and recommendations here? I am definitely not the first dev who does suspending work in a transactional method in Kotlin, and still I am not able to find helpful resources on this issue.
Thanks guys!
TransactionInterceptorto support coroutine, the approach I took was to restoreTransacionSyncronizationManagerstate using aThreadContextElement. The spring has a large codebase therefore I'm restrict the exploration to JPA context, for reference if anyone would like to go deeper and improve the code could be found here it's NOT production ready but could be used to explore.