Skip to content

Commit 2b3767e

Browse files
committed
feat: Implement Spring JDBC compatibility in the SpringTransactionManager
This means that @transactional when using Exposed with Spring also makes Spring JDBC classes like JdbcTemplate partake in the transaction
1 parent 90739da commit 2b3767e

File tree

6 files changed

+642
-10
lines changed

6 files changed

+642
-10
lines changed

spring-transaction/src/main/kotlin/org/jetbrains/exposed/spring/SpringTransactionManager.kt

+62-9
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ import org.jetbrains.exposed.sql.addLogger
88
import org.jetbrains.exposed.sql.exposedLogger
99
import org.jetbrains.exposed.sql.transactions.TransactionManager
1010
import org.jetbrains.exposed.sql.transactions.transactionManager
11+
import org.springframework.jdbc.datasource.ConnectionHandle
12+
import org.springframework.jdbc.datasource.ConnectionHolder
13+
import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport
14+
import org.springframework.transaction.CannotCreateTransactionException
1115
import org.springframework.transaction.TransactionDefinition
1216
import org.springframework.transaction.TransactionSystemException
1317
import org.springframework.transaction.support.AbstractPlatformTransactionManager
1418
import org.springframework.transaction.support.DefaultTransactionStatus
15-
import org.springframework.transaction.support.SmartTransactionObject
19+
import org.springframework.transaction.support.TransactionSynchronizationManager
20+
import org.springframework.transaction.support.TransactionSynchronizationUtils
21+
import java.sql.Connection
1622
import javax.sql.DataSource
1723

1824
/**
@@ -25,13 +31,12 @@ import javax.sql.DataSource
2531
* @sample org.jetbrains.exposed.spring.TestConfig
2632
*/
2733
class SpringTransactionManager(
28-
dataSource: DataSource,
34+
private val dataSource: DataSource,
2935
databaseConfig: DatabaseConfig = DatabaseConfig {},
3036
private val showSql: Boolean = false,
3137
) : AbstractPlatformTransactionManager() {
3238

3339
private var _database: Database
34-
3540
private var _transactionManager: TransactionManager
3641

3742
private val threadLocalTransactionManager: TransactionManager
@@ -63,16 +68,23 @@ class SpringTransactionManager(
6368
manager = threadLocalTransactionManager,
6469
outerManager = outerManager,
6570
outerTransaction = outer,
66-
)
71+
).apply {
72+
setConnectionHolder(
73+
TransactionSynchronizationManager.getResource(dataSource) as? ConnectionHolder
74+
)
75+
}
6776
}
6877

6978
override fun doSuspend(transaction: Any): Any {
7079
val trxObject = transaction as ExposedTransactionObject
7180
val currentManager = trxObject.manager
7281

82+
trxObject.setConnectionHolder(null)
83+
7384
return SuspendedObject(
7485
transaction = currentManager.currentOrNull() as Transaction,
7586
manager = currentManager,
87+
connectionHolder = TransactionSynchronizationManager.unbindResource(dataSource) as ConnectionHolder,
7688
).apply {
7789
currentManager.bindTransactionToThread(null)
7890
TransactionManager.resetCurrent(null)
@@ -84,11 +96,13 @@ class SpringTransactionManager(
8496

8597
TransactionManager.resetCurrent(suspendedObject.manager)
8698
threadLocalTransactionManager.bindTransactionToThread(suspendedObject.transaction)
99+
TransactionSynchronizationManager.bindResource(dataSource, suspendedObject.connectionHolder)
87100
}
88101

89102
private data class SuspendedObject(
90103
val transaction: Transaction,
91-
val manager: TransactionManager
104+
val manager: TransactionManager,
105+
val connectionHolder: ConnectionHolder,
92106
)
93107

94108
override fun isExistingTransaction(transaction: Any): Boolean {
@@ -102,7 +116,7 @@ class SpringTransactionManager(
102116
val currentTransactionManager = trxObject.manager
103117
TransactionManager.resetCurrent(threadLocalTransactionManager)
104118

105-
currentTransactionManager.newTransaction(
119+
val transaction = currentTransactionManager.newTransaction(
106120
isolation = definition.isolationLevel,
107121
readOnly = definition.isReadOnly,
108122
outerTransaction = currentTransactionManager.currentOrNull()
@@ -115,6 +129,24 @@ class SpringTransactionManager(
115129
addLogger(StdOutSqlLogger)
116130
}
117131
}
132+
133+
@Suppress("TooGenericExceptionCaught")
134+
try {
135+
if (!trxObject.hasConnectionHolder()) {
136+
trxObject.connectionHolder = ConnectionHolder(ExposedConnectionHandle(transaction))
137+
trxObject.isNewConnectionHolder = true
138+
}
139+
140+
trxObject.getConnectionHolder().isSynchronizedWithTransaction = true
141+
142+
// Bind the connection holder to the thread.
143+
if (trxObject.isNewConnectionHolder) {
144+
TransactionSynchronizationManager.bindResource(dataSource, trxObject.getConnectionHolder())
145+
}
146+
} catch (ex: Throwable) {
147+
trxObject.setConnectionHolder(null)
148+
throw CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex)
149+
}
118150
}
119151

120152
override fun doCommit(status: DefaultTransactionStatus) {
@@ -135,8 +167,13 @@ class SpringTransactionManager(
135167
trxObject.cleanUpTransactionIfIsPossible {
136168
closeStatementsAndConnections(it)
137169
}
138-
139170
trxObject.setCurrentToOuter()
171+
172+
if (trxObject.isNewConnectionHolder) {
173+
TransactionSynchronizationManager.unbindResource(dataSource)
174+
trxObject.getConnectionHolder().released()
175+
}
176+
trxObject.getConnectionHolder().clear()
140177
}
141178

142179
private fun closeStatementsAndConnections(transaction: Transaction) {
@@ -169,9 +206,17 @@ class SpringTransactionManager(
169206
val manager: TransactionManager,
170207
val outerManager: TransactionManager,
171208
private val outerTransaction: Transaction?,
172-
) : SmartTransactionObject {
209+
) : JdbcTransactionObjectSupport() {
173210

174211
private var isRollback: Boolean = false
212+
var isNewConnectionHolder: Boolean = false
213+
214+
// the Java base class has asymmetric nullability for its connectionHolder getters
215+
// and setters - which confuses the Kotlin compiler and makes it produce warnings/suggestions
216+
// regardless of which style you choose. To avoid it we override this.
217+
override fun setConnectionHolder(connectionHolder: ConnectionHolder?) {
218+
super.setConnectionHolder(connectionHolder)
219+
}
175220

176221
fun cleanUpTransactionIfIsPossible(block: (transaction: Transaction) -> Unit) {
177222
val currentTransaction = getCurrentTransaction()
@@ -212,7 +257,15 @@ class SpringTransactionManager(
212257
override fun isRollbackOnly() = isRollback
213258

214259
override fun flush() {
215-
// Do noting
260+
TransactionSynchronizationUtils.triggerFlush()
261+
}
262+
}
263+
264+
class ExposedConnectionHandle(
265+
val transaction: Transaction
266+
) : ConnectionHandle {
267+
override fun getConnection(): Connection {
268+
return transaction.connection.connection as Connection
216269
}
217270
}
218271
}

0 commit comments

Comments
 (0)