Skip to content

Conversation

@bystam
Copy link
Contributor

@bystam bystam commented Dec 1, 2025

Description

The Exposed SpringTransactionManager currently does not make Spring JDBC constructs take part in the transaction. In short:

@Autowired
private lateinit var dataSource: DataSource

@Transactional
fun insertTwoThings() {
    val jdbcTemplate = JdbcTemplate(dataSource)
    // these two statements will NOT be run in a transaction
    jdbcTemplate.executeUpdate("INSERT INTO foo VALUES ('bar1')")
    jdbcTemplate.executeUpdate("INSERT INTO foo VALUES ('bar2')")
}

This draft tries to start supporting this by adding functionality to SpringTransactionManager borrowed from the basic DataSourceTransactionManager.

Detailed description:
Caveat: I am not an expert on neither Exposed or Spring JDBC. This is my best effort attempt at simply reading source code.

  • In short, Spring JDBC manages its transactions by pinning ConnectionHolder instances to thread locals inside the TransactionSynchronizationManager. Spring JDBC classes like JdbcTemplate then reuse those ConnectionHolder instances to make sure a single autocommit-disabled connection is shared throughout the transaction
  • Since the Exposed SpringTransactionManager does not interact with TransactionSynchronizationManager at all - it does not influence transaction semantics in Spring JDBC.
  • This PR makes a quick attempt at blending SpringTransactionManager and DataSourceTransactionManager in order to make @Transactional influence both Exposed and Spring JDBC simultaneously.

But why?
Since Exposed depends on spring-jdbc, in my mind it makes sense that it would honor the transaction semantics of its dependency. In comparison, when using JPA/Hibernate in Spring, the JpaTransactionManager introduces JPA-specific transaction behaviour but ALSO make sure to adhere to the expected semantics of Spring JDBC.

Copy submitted
I originally had this submitted here: #2400 but that one had the source branch set to the main branch of my fork, which goes against the suggested review guidelines AFAIK and also made it extra complicated for me to sync the main repo into my fork.


Type of Change

Please mark the relevant options with an "X":

  • Bug fix
  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • MariaDB
  • Mysql5
  • Mysql8
  • Oracle
  • Postgres
  • SqlServer
  • H2
  • SQLite

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)
  • All public methods affected by my PR has up to date API docs
  • Documentation for my change is up to date

Related Issues

https://youtrack.jetbrains.com/issue/EXPOSED-657/Transaction-manager-provided-by-Exposed-is-not-compatible-with-JdbcTemplate

…ager

This means that @transactional when using Exposed with Spring also makes Spring JDBC classes like JdbcTemplate partake in the transaction
…o the shared Spring-test database configuration
… as it seems like TransactionSynchronizaationUtils.triggerFlush might be for other things
…e SmartTransactionObject over JdbcTransactionObjectSupport
@bystam bystam marked this pull request as draft December 1, 2025 13:54

@OptIn(InternalApi::class)
ThreadLocalTransactionsStack.pushTransaction(suspendedObject.transaction)
TransactionSynchronizationManager.bindResource(dataSource, suspendedObject.connectionHolder)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see in doSuspend() the line trxObject.connectionHolder = null, so
should be added here trxObject.connectionHolder = suspendedObject.connectionHolder?

Could it cause missing of connectionHolder in trxObject after one suspend/resume cycle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that it looks strange from a pure symmetry point of view. It was a while ago that I actually built this but I went back and looked at the thing I modeled this after (the built-in Spring DataSourceTransactionManager).

In there, they don't seem to re-connect the connection holder from suspended object to the transaction object.

See screenshot:
Image

Also, I did some digging with the debugger. I hope this screenshot will be readable. But looking at the actual underlying arguments sent to doResume, it looks like the transaction and the suspension objects already contain the same connection holder - so I assume those lifecycle elements are already handled by AbstractPlatformTransactionManager.

Image

} catch (ex: Throwable) {
trxObject.connectionHolder = null
throw CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move the block

@OptIn(InternalApi::class)
ThreadLocalTransactionsStack.pushTransaction(newTransaction)

to the end of this method?

I see a problem that Exposed transaction could be added to the stack, and lost there in the case of exception inside this block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely! Good idea!

@obabichevjb
Copy link
Collaborator

@bystam Hi, thank you for the PR. Overall, it looks like a good idea to allow using Exposed’s transactions with other data sources inside Spring. I’m happy to merge it if there are no further concerns.

I’ve left some comments in the review — please take a look when you have a moment.

@obabichevjb obabichevjb requested a review from bog-walk December 4, 2025 11:28
…'doBegin' to avoid it leaking on an exception thrown when setting the spring trx resources
@bystam bystam requested a review from obabichevjb December 5, 2025 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants