Skip to content

Commit fa73510

Browse files
authored
feat!: EXPOSED-109 change implement of spring transaction manager (#1840)
* refactor: Implement a new one with AbstractPlatformTransactionManager * refactor: remove unused property * fix: remove nullable * fix: add show sql feature * fix: rebuild api file * chore: remove property * chore: add default show sql value * fix: api definition * feat: add default database config value * feat :Add SmartTranscationObject implementation * feat: close connection when transaction end * chore: remove unused values * feat: reset outer tx manager when transaction clean up * test: Add outer transaction manager setting exception test * chore: Add import * test: add manager setting test * chore: remake api file * fix: fix when commit or rollback failure * chore: fix detekt issue * test: Add transactionAwareDataSourceProxy test * chore: refactor spring transaction manager test * refactor: remove default value in test * refactor: apply code review * refactor: apply test code review * refactor: merge duplicate tests * refactor: remove mockk and change exposed transaction object to private * refactor: move database to private * refactor: clean up transaction manager after test ended * fix: while condition * test: Add exposed and spring combine transaction * fix: test * fix: api dump * refactor: apply connection spy suggestion * refactor: apply code review * refactor: apply code reivew
1 parent 5199654 commit fa73510

File tree

8 files changed

+523
-145
lines changed

8 files changed

+523
-145
lines changed
Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
public final class org/jetbrains/exposed/spring/SpringTransactionManager : org/springframework/jdbc/datasource/DataSourceTransactionManager, org/jetbrains/exposed/sql/transactions/TransactionManager {
2-
public fun <init> (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;ZZIJJ)V
3-
public synthetic fun <init> (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;ZZIJJILkotlin/jvm/internal/DefaultConstructorMarker;)V
4-
public fun bindTransactionToThread (Lorg/jetbrains/exposed/sql/Transaction;)V
5-
public fun currentOrNull ()Lorg/jetbrains/exposed/sql/Transaction;
6-
public fun getDefaultIsolationLevel ()I
7-
public fun getDefaultMaxRepetitionDelay ()J
8-
public fun getDefaultMinRepetitionDelay ()J
9-
public fun getDefaultReadOnly ()Z
10-
public fun getDefaultRepetitionAttempts ()I
11-
public fun newTransaction (IZLorg/jetbrains/exposed/sql/Transaction;)Lorg/jetbrains/exposed/sql/Transaction;
12-
public fun setDefaultIsolationLevel (I)V
13-
public fun setDefaultMaxRepetitionDelay (J)V
14-
public fun setDefaultMinRepetitionDelay (J)V
15-
public fun setDefaultReadOnly (Z)V
16-
public fun setDefaultRepetitionAttempts (I)V
1+
public final class org/jetbrains/exposed/spring/SpringTransactionManager : org/springframework/transaction/support/AbstractPlatformTransactionManager {
2+
public fun <init> (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;Z)V
3+
public synthetic fun <init> (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/sql/DatabaseConfig;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
174
}
185

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

Lines changed: 111 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -6,182 +6,170 @@ import org.jetbrains.exposed.sql.StdOutSqlLogger
66
import org.jetbrains.exposed.sql.Transaction
77
import org.jetbrains.exposed.sql.addLogger
88
import org.jetbrains.exposed.sql.exposedLogger
9-
import org.jetbrains.exposed.sql.statements.api.ExposedConnection
10-
import org.jetbrains.exposed.sql.statements.jdbc.JdbcConnectionImpl
11-
import org.jetbrains.exposed.sql.transactions.TransactionInterface
129
import org.jetbrains.exposed.sql.transactions.TransactionManager
13-
import org.springframework.jdbc.datasource.ConnectionHolder
14-
import org.springframework.jdbc.datasource.DataSourceTransactionManager
10+
import org.jetbrains.exposed.sql.transactions.transactionManager
1511
import org.springframework.transaction.TransactionDefinition
1612
import org.springframework.transaction.TransactionSystemException
17-
import org.springframework.transaction.support.DefaultTransactionDefinition
13+
import org.springframework.transaction.support.AbstractPlatformTransactionManager
1814
import org.springframework.transaction.support.DefaultTransactionStatus
19-
import org.springframework.transaction.support.TransactionSynchronizationManager
15+
import org.springframework.transaction.support.SmartTransactionObject
2016
import javax.sql.DataSource
2117

2218
class SpringTransactionManager(
2319
dataSource: DataSource,
24-
databaseConfig: DatabaseConfig = DatabaseConfig { },
20+
databaseConfig: DatabaseConfig = DatabaseConfig {},
2521
private val showSql: Boolean = false,
26-
@Volatile override var defaultReadOnly: Boolean = databaseConfig.defaultReadOnly,
27-
@Volatile override var defaultRepetitionAttempts: Int = databaseConfig.defaultRepetitionAttempts,
28-
@Volatile override var defaultMinRepetitionDelay: Long = databaseConfig.defaultMinRepetitionDelay,
29-
@Volatile override var defaultMaxRepetitionDelay: Long = databaseConfig.defaultMaxRepetitionDelay
30-
) : DataSourceTransactionManager(dataSource), TransactionManager {
22+
) : AbstractPlatformTransactionManager() {
3123

32-
init {
33-
this.isRollbackOnCommitFailure = true
34-
}
35-
36-
private val db = Database.connect(
37-
datasource = dataSource,
38-
databaseConfig = databaseConfig
39-
) { this }
24+
private var _database: Database
4025

41-
@Volatile
42-
override var defaultIsolationLevel: Int = -1
43-
get() {
44-
if (field == -1) {
45-
field = Database.getDefaultIsolationLevel(db)
46-
}
47-
return field
48-
}
26+
private var _transactionManager: TransactionManager
4927

50-
private val transactionStackKey = "SPRING_TRANSACTION_STACK_KEY"
28+
private val threadLocalTransactionManager: TransactionManager
29+
get() = _transactionManager
5130

52-
private fun getTransactionStack(): List<TransactionManager> {
53-
return TransactionSynchronizationManager.getResource(transactionStackKey)
54-
?.let { it as List<TransactionManager> }
55-
?: listOf()
31+
init {
32+
_database = Database.connect(
33+
datasource = dataSource, databaseConfig = databaseConfig
34+
).apply {
35+
_transactionManager = this.transactionManager
36+
}
5637
}
5738

58-
private fun setTransactionStack(list: List<TransactionManager>) {
59-
TransactionSynchronizationManager.unbindResourceIfPossible(transactionStackKey)
60-
TransactionSynchronizationManager.bindResource(transactionStackKey, list)
61-
}
39+
override fun doGetTransaction(): Any {
40+
val outerManager = TransactionManager.manager
41+
val outer = threadLocalTransactionManager.currentOrNull()
6242

63-
private fun pushTransactionStack(transaction: TransactionManager) {
64-
val transactionList = getTransactionStack()
65-
setTransactionStack(transactionList + transaction)
43+
return ExposedTransactionObject(
44+
manager = threadLocalTransactionManager,
45+
outerManager = outerManager,
46+
outerTransaction = outer,
47+
)
6648
}
6749

68-
private fun popTransactionStack() = setTransactionStack(getTransactionStack().dropLast(1))
69-
70-
private fun getLastTransactionStack() = getTransactionStack().lastOrNull()
71-
7250
override fun doBegin(transaction: Any, definition: TransactionDefinition) {
73-
super.doBegin(transaction, definition)
51+
val trxObject = transaction as ExposedTransactionObject
52+
53+
val currentTransactionManager = trxObject.manager
54+
TransactionManager.resetCurrent(currentTransactionManager)
7455

75-
if (TransactionSynchronizationManager.hasResource(obtainDataSource())) {
76-
currentOrNull() ?: initTransaction(transaction)
56+
currentTransactionManager.currentOrNull() ?: currentTransactionManager.newTransaction(
57+
isolation = definition.isolationLevel,
58+
readOnly = definition.isReadOnly,
59+
).apply {
60+
if (showSql) {
61+
addLogger(StdOutSqlLogger)
62+
}
7763
}
64+
}
7865

79-
pushTransactionStack(this@SpringTransactionManager)
66+
override fun doCommit(status: DefaultTransactionStatus) {
67+
val trxObject = status.transaction as ExposedTransactionObject
68+
TransactionManager.resetCurrent(trxObject.manager)
69+
trxObject.commit()
8070
}
8171

82-
override fun doCleanupAfterCompletion(transaction: Any) {
83-
super.doCleanupAfterCompletion(transaction)
84-
if (!TransactionSynchronizationManager.hasResource(obtainDataSource())) {
85-
TransactionSynchronizationManager.unbindResourceIfPossible(this)
86-
}
72+
override fun doRollback(status: DefaultTransactionStatus) {
73+
val trxObject = status.transaction as ExposedTransactionObject
74+
TransactionManager.resetCurrent(trxObject.manager)
75+
trxObject.rollback()
76+
}
8777

88-
popTransactionStack()
89-
TransactionManager.resetCurrent(getLastTransactionStack())
78+
override fun doCleanupAfterCompletion(transaction: Any) {
79+
val trxObject = transaction as ExposedTransactionObject
9080

91-
if (TransactionSynchronizationManager.isSynchronizationActive() && TransactionSynchronizationManager.getSynchronizations().isEmpty()) {
92-
TransactionSynchronizationManager.clearSynchronization()
81+
trxObject.cleanUpTransactionIfIsPossible {
82+
closeStatementsAndConnections(it)
9383
}
94-
}
9584

96-
override fun doSuspend(transaction: Any): Any {
97-
TransactionSynchronizationManager.unbindResourceIfPossible(this)
98-
return super.doSuspend(transaction)
85+
trxObject.setCurrentToOuter()
9986
}
10087

101-
override fun doCommit(status: DefaultTransactionStatus) {
88+
private fun closeStatementsAndConnections(transaction: Transaction) {
89+
val currentStatement = transaction.currentStatement
10290
@Suppress("TooGenericExceptionCaught")
10391
try {
104-
currentOrNull()?.commit()
105-
} catch (e: Exception) {
106-
throw TransactionSystemException(e.message.orEmpty(), e)
92+
currentStatement?.let {
93+
it.closeIfPossible()
94+
transaction.currentStatement = null
95+
}
96+
transaction.closeExecutedStatements()
97+
} catch (error: Exception) {
98+
exposedLogger.warn("Statements close failed", error)
10799
}
108-
}
109100

110-
override fun doRollback(status: DefaultTransactionStatus) {
111101
@Suppress("TooGenericExceptionCaught")
112102
try {
113-
currentOrNull()?.rollback()
114-
} catch (e: Exception) {
115-
throw TransactionSystemException(e.message.orEmpty(), e)
103+
transaction.close()
104+
} catch (error: Exception) {
105+
exposedLogger.warn("Transaction close failed: ${error.message}. Statement: $currentStatement", error)
116106
}
117107
}
118108

119-
override fun newTransaction(isolation: Int, readOnly: Boolean, outerTransaction: Transaction?): Transaction {
120-
val tDefinition = DefaultTransactionDefinition().apply {
121-
isReadOnly = readOnly
122-
isolationLevel = isolation
123-
}
124-
125-
val transactionStatus = (getTransaction(tDefinition) as DefaultTransactionStatus)
126-
return currentOrNull() ?: initTransaction(transactionStatus.transaction)
109+
override fun doSetRollbackOnly(status: DefaultTransactionStatus) {
110+
val trxObject = status.transaction as ExposedTransactionObject
111+
trxObject.setRollbackOnly()
127112
}
128113

129-
private fun initTransaction(transaction: Any): Transaction {
130-
val connection = (TransactionSynchronizationManager.getResource(obtainDataSource()) as ConnectionHolder).connection
114+
private data class ExposedTransactionObject(
115+
val manager: TransactionManager,
116+
val outerManager: TransactionManager,
117+
private val outerTransaction: Transaction?,
118+
) : SmartTransactionObject {
131119

132-
@Suppress("TooGenericExceptionCaught")
133-
val transactionImpl = try {
134-
SpringTransaction(JdbcConnectionImpl(connection), db, defaultIsolationLevel, defaultReadOnly, currentOrNull(), transaction)
135-
} catch (e: Exception) {
136-
exposedLogger.error("Failed to start transaction. Connection will be closed.", e)
137-
connection.close()
138-
throw e
139-
}
120+
private var isRollback: Boolean = false
121+
private var isCurrentTransactionEnded: Boolean = false
140122

141-
TransactionManager.resetCurrent(this)
142-
return Transaction(transactionImpl).apply {
143-
TransactionSynchronizationManager.bindResource(this@SpringTransactionManager, this)
144-
if (showSql) {
145-
addLogger(StdOutSqlLogger)
123+
fun cleanUpTransactionIfIsPossible(block: (transaction: Transaction) -> Unit) {
124+
val currentTransaction = getCurrentTransaction()
125+
if (isCurrentTransactionEnded && currentTransaction != null) {
126+
block(currentTransaction)
146127
}
147128
}
148-
}
149129

150-
override fun currentOrNull(): Transaction? = TransactionSynchronizationManager.getResource(this) as Transaction?
151-
override fun bindTransactionToThread(transaction: Transaction?) {
152-
if (transaction != null) {
153-
bindResourceForSure(this, transaction)
154-
} else {
155-
TransactionSynchronizationManager.unbindResourceIfPossible(this)
130+
fun setCurrentToOuter() {
131+
manager.bindTransactionToThread(outerTransaction)
132+
TransactionManager.resetCurrent(outerManager)
156133
}
157-
}
158134

159-
private fun bindResourceForSure(key: Any, value: Any) {
160-
TransactionSynchronizationManager.unbindResourceIfPossible(key)
161-
TransactionSynchronizationManager.bindResource(key, value)
162-
}
163-
164-
private inner class SpringTransaction(
165-
override val connection: ExposedConnection<*>,
166-
override val db: Database,
167-
override val transactionIsolation: Int,
168-
override val readOnly: Boolean,
169-
override val outerTransaction: Transaction?,
170-
private val currentTransaction: Any,
171-
) : TransactionInterface {
172-
173-
override fun commit() {
174-
connection.commit()
135+
private fun hasOuterTransaction(): Boolean {
136+
return outerTransaction != null
175137
}
176138

177-
override fun rollback() {
178-
connection.rollback()
139+
@Suppress("TooGenericExceptionCaught")
140+
fun commit() {
141+
try {
142+
if (hasOuterTransaction().not()) {
143+
isCurrentTransactionEnded = true
144+
manager.currentOrNull()?.commit()
145+
}
146+
} catch (error: Exception) {
147+
throw TransactionSystemException(error.message.orEmpty(), error)
148+
}
179149
}
180150

181-
override fun close() {
182-
if (TransactionSynchronizationManager.isActualTransactionActive()) {
183-
this@SpringTransactionManager.doCleanupAfterCompletion(currentTransaction)
151+
@Suppress("TooGenericExceptionCaught")
152+
fun rollback() {
153+
try {
154+
if (hasOuterTransaction().not()) {
155+
isCurrentTransactionEnded = true
156+
manager.currentOrNull()?.rollback()
157+
}
158+
} catch (error: Exception) {
159+
throw TransactionSystemException(error.message.orEmpty(), error)
184160
}
185161
}
162+
163+
fun getCurrentTransaction(): Transaction? = manager.currentOrNull()
164+
165+
fun setRollbackOnly() {
166+
isRollback = true
167+
}
168+
169+
override fun isRollbackOnly() = isRollback
170+
171+
override fun flush() {
172+
// Do noting
173+
}
186174
}
187175
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.jetbrains.exposed.spring
2+
3+
import java.sql.Connection
4+
import java.sql.Savepoint
5+
6+
internal class ConnectionSpy(private val connection: Connection) : Connection by connection {
7+
8+
var commitCallCount: Int = 0
9+
var rollbackCallCount: Int = 0
10+
var closeCallCount: Int = 0
11+
var mockReadOnly: Boolean = false
12+
var mockIsClosed: Boolean = false
13+
var mockAutoCommit: Boolean = false
14+
var mockTransactionIsolation: Int = Connection.TRANSACTION_READ_COMMITTED
15+
var mockCommit: () -> Unit = {}
16+
var mockRollback: () -> Unit = {}
17+
private val callOrder = mutableListOf<String>()
18+
19+
fun verifyCallOrder(vararg functions: String): Boolean {
20+
val indices = functions.map { callOrder.indexOf(it) }
21+
return indices.none { it == -1 } && indices == indices.sorted()
22+
}
23+
24+
fun clearMock() {
25+
commitCallCount = 0
26+
rollbackCallCount = 0
27+
closeCallCount = 0
28+
mockAutoCommit = false
29+
mockReadOnly = false
30+
mockIsClosed = false
31+
mockTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED
32+
mockCommit = {}
33+
mockRollback = {}
34+
callOrder.clear()
35+
}
36+
37+
override fun close() {
38+
callOrder.add("close")
39+
closeCallCount++
40+
}
41+
42+
override fun setAutoCommit(autoCommit: Boolean) {
43+
callOrder.add("setAutoCommit")
44+
mockAutoCommit = autoCommit
45+
}
46+
47+
override fun getAutoCommit() = mockAutoCommit
48+
override fun commit() {
49+
callOrder.add("commit")
50+
commitCallCount++
51+
mockCommit()
52+
}
53+
54+
override fun rollback() {
55+
callOrder.add("rollback")
56+
rollbackCallCount++
57+
mockRollback()
58+
}
59+
60+
override fun rollback(savepoint: Savepoint?) = Unit
61+
override fun isClosed(): Boolean {
62+
callOrder.add("isClosed")
63+
return mockIsClosed
64+
}
65+
66+
override fun setReadOnly(readOnly: Boolean) {
67+
callOrder.add("setReadOnly")
68+
mockReadOnly = readOnly
69+
}
70+
71+
override fun isReadOnly(): Boolean {
72+
callOrder.add("isReadOnly")
73+
return mockReadOnly
74+
}
75+
76+
override fun getTransactionIsolation() = mockTransactionIsolation
77+
}

0 commit comments

Comments
 (0)