-
Notifications
You must be signed in to change notification settings - Fork 700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Support regular spring JDBC transactions in the Exposed SpringTransactionManager
#2400
base: main
Are you sure you want to change the base?
Conversation
SpringTransactionManager
SpringTransactionManager
Apologize for the failing PR title/commit message/detekt checks. I will make sure to get that sorted. |
).apply { | ||
setConnectionHolder( | ||
TransactionSynchronizationManager.getResource(dataSource) as? ConnectionHolder | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Detect any existing ConnectionHolder
set as a thread-local and reuse it
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Show resolved
Hide resolved
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Outdated
Show resolved
Hide resolved
spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt
Outdated
Show resolved
Hide resolved
val transaction: Transaction | ||
) : ConnectionHandle { | ||
override fun getConnection(): Connection { | ||
return transaction.connection.connection as Connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is actually more or less the thing that makes the whole implementation work.
For Exposed and Spring JDBC to co-exist, they need to both see the same connection. Spring JDBC fetches its connection by calling something like:
ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
holder.getConnection()
so for them to share it, the Exposed transaction manager needs to install a ConnectionHolder
that provides the same connection using TransactionSynchronizationManager.bindResource(dataSource, <some connection holder>)
. The ConnectionHolder
class of spring can take a "pointer to a connection" called a ConnectionHandle
, which is what I implement here.
This is done upstairs like
trxObject.setConnectionHolder(
ConnectionHolder(ExposedConnectionHandle(transaction))
)
trxObject.isNewConnectionHolder = true
...
if (trxObject.isNewConnectionHolder){
TransactionSynchronizationManager.bindResource(dataSource, trxObject.getConnectionHolder())
}
trxObject.setConnectionHolder( | ||
ConnectionHolder(ExposedConnectionHandle(transaction)) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ExposedConnectionHandle
is the thing that makes this all work. See comment downstairs.
I obviously broke a bunch of other tests using this implementation 🙃 - looking into it |
Oops, didn't see your comment above, I left a comment because the detekt test failed at build time. |
Okay - my attempt broke quite a lot of tests it looks like. Might have been a bit naive to simply blend them like I did. Maybe things need to be integrated more deeply into the Exposed |
When I first proposed to re-implement the I used ThreadLocalTransactionManager to integrate with Spring, but it seemed a bit too much to fulfill the basic requirements of Spring, such as the SQLException not rollback issue. On second thought, it would be nice to rewrite the TransactionManager implementation, even if it's difficult. |
SpringTransactionManager
SpringTransactionManager
Looking more closely at the failing tests - I got the sense that all the "extra stuff" that I cherry-picked from the I decided to scratch most of that, since the regular Exposed What's left is simply the The only bit I am curious about is whether it's important to set the |
I see! I think I managed to get it to work now actually. Though I am not entirely sure how I would write a test that might catch any nasty edge cases. All the existing tests and the new ones I added are green, though! |
I created a copy of the existing Some of them behaved differently, but that is intentional. Exposed requires a transaction in order to operate, but |
import org.springframework.transaction.support.TransactionSynchronizationManager | ||
import kotlin.test.Test | ||
|
||
class TransactionSynchronizationTest : SpringTransactionTestBase() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behaviour is somewhat tricky. The full TransactionSynchronization
behaviour is actually not supported right now - because we handle savepoints outside of Springs knowledge, which means that the savepoint callbacks specifically do not trigger.
While that might be a somewhat incomplete support - I personally feel that covering 95% of the common use cases is better than not covering them, as listening to savepoint callbacks should be a quite niche thing.
Hi @bystam, thank you for your contribution. Please rebase on the |
d671f78
to
2b3767e
Compare
…ager This means that @transactional when using Exposed with Spring also makes Spring JDBC classes like JdbcTemplate partake in the transaction
I feel like I made all the checks pass now - but it looks like some tests failed on the latest run. Those kinds of failures happen to me sporadically locally too, howver. And I don't think I caused them? Or? 🤔 Edit: I found it. An existing test accidentally had a teardown that unregistered the database set up by the base It failed sporadically because the tests were run in different order every time - and they only failed when they are run in a specific order. |
…o the shared Spring-test database configuration
… as it seems like TransactionSynchronizaationUtils.triggerFlush might be for other things
…e SmartTransactionObject over JdbcTransactionObjectSupport
Description
The Exposed
SpringTransactionManager
currently does not make Spring JDBC constructs take part in the transaction. In short:This draft tries to start supporting this by adding functionality to
SpringTransactionManager
borrowed from the basicDataSourceTransactionManager
.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.
ConnectionHolder
instances to thread locals inside theTransactionSynchronizationManager
. Spring JDBC classes likeJdbcTemplate
then reuse thoseConnectionHolder
instances to make sure a single autocommit-disabled connection is shared throughout the transactionSpringTransactionManager
does not interact withTransactionSynchronizationManager
at all - it does not influence transaction semantics in Spring JDBC.SpringTransactionManager
andDataSourceTransactionManager
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, theJpaTransactionManager
introduces JPA-specific transaction behaviour but ALSO make sure to adhere to the expected semantics of Spring JDBC.Why a draft?
This is written by basically cherry-picking
DataSourceTransactionManager
code intoSpringTransactionManager
. I feel like I managed to get a pretty decent high level understanding of how Spring JDBC manages transactions, but there's plenty of low level stuff in there I am uncertain about whether it made sense to bring or not.Type of Change
Please mark the relevant options with an "X":
Updates/remove existing public API methods:
Affected databases:
Checklist
Related Issues
https://youtrack.jetbrains.com/issue/EXPOSED-657/Transaction-manager-provided-by-Exposed-is-not-compatible-with-JdbcTemplate