Skip to content

Commit

Permalink
support SqlException translate
Browse files Browse the repository at this point in the history
  • Loading branch information
endink committed Oct 1, 2024
1 parent bf3bf79 commit 7860774
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 221 deletions.
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ object Versions {
const val infraBomVersion = "3.2.0"
const val infraPluginVersion = "2.0.+"

const val exposedVersion = "0.53.0"
const val exposedVersion = "0.55.0"

const val kotlinPoetVersion = "1.18.1"
const val kotlinCompileTesting = "0.5.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package com.labijie.infra.orm

import com.labijie.infra.orm.configuration.InfraExposedProperties
import com.labijie.infra.orm.interceptor.InfraStatementInterceptor
import org.jetbrains.exposed.sql.SqlLogger
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.addLogger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class InfraExposedAutoConfiguration : ApplicationContextAware {
dataSource: DataSource
): JdbcExposedTransactionManager {

val txm = JdbcExposedTransactionManager(dataSource, databaseConfig, false)
val txm = SpringTransactionManager(dataSource, databaseConfig, false)
txm.addListener(ExposedTransactionListener(environment, properties))
return txm
return JdbcExposedTransactionManager(properties, txm)
}

class ExposedTableRegistrar : BeanFactoryAware, ImportBeanDefinitionRegistrar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty
data class InfraExposedProperties(
var showSql: Boolean = false,

val translateSqlException: Boolean = true,

@NestedConfigurationProperty
val generateSchema: SchemaGenerationSettings = SchemaGenerationSettings(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,243 +5,60 @@
package com.labijie.infra.orm.configuration


import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transactionManager
import org.springframework.dao.DataAccessException
import org.springframework.jdbc.support.SQLExceptionSubclassTranslator
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
import org.springframework.jdbc.support.SQLExceptionTranslator
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.TransactionSystemException
import org.springframework.transaction.support.AbstractPlatformTransactionManager
import org.springframework.transaction.support.DefaultTransactionStatus
import org.springframework.transaction.support.SmartTransactionObject
import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager
import org.springframework.transaction.support.TransactionCallback
import java.sql.SQLException
import javax.sql.DataSource

/**
* Transaction Manager implementation that builds on top of Spring's standard transaction workflow.
*
* @param dataSource The data source that produces `Connection` objects.
* @param databaseConfig The configuration that defines custom properties to be used with connections.
* If none is specified, the default configuration values will be used.
* @property showSql Whether transaction queries should be logged. Defaults to `false`.
*/
open class JdbcExposedTransactionManager(
dataSource: DataSource,
databaseConfig: DatabaseConfig = DatabaseConfig {},
private val showSql: Boolean = false,
) : AbstractPlatformTransactionManager() {

private var _database: Database

private var _transactionManager: TransactionManager

private var exceptionTranslator: SQLExceptionTranslator = SQLExceptionSubclassTranslator()

private val threadLocalTransactionManager: TransactionManager
get() = _transactionManager

init {
_database = Database.connect(
datasource = dataSource, databaseConfig = databaseConfig
).apply {
_transactionManager = this.transactionManager
}

isNestedTransactionAllowed = databaseConfig.useNestedTransactions
}
class JdbcExposedTransactionManager(
private val properties: InfraExposedProperties,
private val transactionManager: PlatformTransactionManager) : CallbackPreferringPlatformTransactionManager {

/**
* ExposedConnection implements savepoint by itself
* `useSavepointForNestedTransaction` is use `SavepointManager` for nested transaction
*
* So we don't need to use java savepoint for nested transaction
*/
override fun useSavepointForNestedTransaction() = false

fun setExceptionTranslator(exceptionTranslator: SQLExceptionTranslator) {
this.exceptionTranslator = exceptionTranslator
}

override fun doGetTransaction(): Any {
val outerManager = TransactionManager.manager
val outer = threadLocalTransactionManager.currentOrNull()

return ExposedTransactionObject(
manager = threadLocalTransactionManager,
outerManager = outerManager,
outerTransaction = outer,
)
}

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

return SuspendedObject(
transaction = currentManager.currentOrNull() as Transaction,
manager = currentManager,
).apply {
currentManager.bindTransactionToThread(null)
TransactionManager.resetCurrent(null)
}
}

override fun doResume(transaction: Any?, suspendedResources: Any) {
val suspendedObject = suspendedResources as SuspendedObject

TransactionManager.resetCurrent(suspendedObject.manager)
threadLocalTransactionManager.bindTransactionToThread(suspendedObject.transaction)
}

private data class SuspendedObject(
val transaction: Transaction,
val manager: TransactionManager
)

override fun isExistingTransaction(transaction: Any): Boolean {
val trxObject = transaction as ExposedTransactionObject
return trxObject.getCurrentTransaction() != null
}

override fun doBegin(transaction: Any, definition: TransactionDefinition) {
val trxObject = transaction as ExposedTransactionObject

val currentTransactionManager = trxObject.manager
TransactionManager.resetCurrent(threadLocalTransactionManager)

currentTransactionManager.newTransaction(
isolation = definition.isolationLevel,
readOnly = definition.isReadOnly,
outerTransaction = currentTransactionManager.currentOrNull()
).apply {
if (definition.timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
queryTimeout = definition.timeout
}

if (showSql) {
addLogger(StdOutSqlLogger)
private var exceptionTranslator: SQLExceptionTranslator = SQLErrorCodeSQLExceptionTranslator()
protected fun translateException(ex: Throwable): RuntimeException {
val translator = this.exceptionTranslator
if(ex is SQLException) {
val dae: DataAccessException? = translator.translate("Exposed Exec", null, ex)
if (dae != null) {
return dae
}
}
throw TransactionSystemException(ex.message.orEmpty(), ex)
}

override fun doCommit(status: DefaultTransactionStatus) {
try {
val trxObject = status.transaction as ExposedTransactionObject
TransactionManager.resetCurrent(trxObject.manager)
trxObject.commit()
}catch (e: Throwable) {
throw this.translateException("Exposed Commit", e)
}
}

override fun doRollback(status: DefaultTransactionStatus) {
try {
val trxObject = status.transaction as ExposedTransactionObject
TransactionManager.resetCurrent(trxObject.manager)
trxObject.rollback()
}catch (e: Throwable) {
throw this.translateException("Exposed Rollback", e)
}
fun setExceptionTranslator(translator: SQLExceptionTranslator) {
this.exceptionTranslator = translator
}

override fun doCleanupAfterCompletion(transaction: Any) {
val trxObject = transaction as ExposedTransactionObject

trxObject.cleanUpTransactionIfIsPossible {
closeStatementsAndConnections(it)
}

trxObject.setCurrentToOuter()
override fun getTransaction(definition: TransactionDefinition?): TransactionStatus {
return this.transactionManager.getTransaction(definition)
}

private fun closeStatementsAndConnections(transaction: Transaction) {
val currentStatement = transaction.currentStatement
@Suppress("TooGenericExceptionCaught")
try {
currentStatement?.let {
it.closeIfPossible()
transaction.currentStatement = null
}
transaction.closeExecutedStatements()
} catch (error: Exception) {
exposedLogger.warn("Statements close failed", error)
}

@Suppress("TooGenericExceptionCaught")
try {
transaction.close()
} catch (error: Exception) {
exposedLogger.warn("Transaction close failed: ${error.message}. Statement: $currentStatement", error)
}
override fun commit(status: TransactionStatus) {
this.transactionManager.commit(status)
}

override fun doSetRollbackOnly(status: DefaultTransactionStatus) {
val trxObject = status.transaction as ExposedTransactionObject
trxObject.setRollbackOnly()
override fun rollback(status: TransactionStatus) {
this.transactionManager.rollback(status)
}


protected fun translateException(task: String, ex: Throwable): RuntimeException {
if(ex is SQLException) {
val dae: DataAccessException? = exceptionTranslator.translate(task, null, ex)
if (dae != null) {
return dae
override fun <T : Any?> execute(definition: TransactionDefinition?, callback: TransactionCallback<T>): T? {
val status = this.transactionManager.getTransaction(definition)
return try {
callback.doInTransaction(status)
}catch (e: Throwable) {
if(properties.translateSqlException) {
throw translateException(e)
}else {
throw e
}
}
throw TransactionSystemException(ex.message.orEmpty(), ex)
}

private data class ExposedTransactionObject(
val manager: TransactionManager,
val outerManager: TransactionManager,
private val outerTransaction: Transaction?,
) : SmartTransactionObject {

private var isRollback: Boolean = false

fun cleanUpTransactionIfIsPossible(block: (transaction: Transaction) -> Unit) {
val currentTransaction = getCurrentTransaction()
if (currentTransaction != null) {
block(currentTransaction)
}
}

fun setCurrentToOuter() {
manager.bindTransactionToThread(outerTransaction)
TransactionManager.resetCurrent(outerManager)
}

@Suppress("TooGenericExceptionCaught")
fun commit() {
try {
manager.currentOrNull()?.commit()
} catch (error: Exception) {
throw TransactionSystemException(error.message.orEmpty(), error)
}
}

@Suppress("TooGenericExceptionCaught")
fun rollback() {
try {
manager.currentOrNull()?.rollback()
} catch (error: Exception) {
throw TransactionSystemException(error.message.orEmpty(), error)
}
}

fun getCurrentTransaction(): Transaction? = manager.currentOrNull()

fun setRollbackOnly() {
isRollback = true
}


override fun isRollbackOnly() = isRollback

override fun flush() {
// Do noting
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
"sourceType": "com.labijie.infra.orm.configuration.InfraExposedProperties",
"description": "show sql for exposed",
"defaultValue": false
},
{
"name": "infra.exposed.translate-sql-exception",
"type": "java.lang.Boolean",
"sourceType": "com.labijie.infra.orm.configuration.InfraExposedProperties",
"description": "Translate sql exception to DataAccessException",
"defaultValue": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @author Anders Xiao
* @date 2024-10-01
*/
package com.labijie.infra.orm.testing

import com.labijie.infra.orm.test.ExposedTest
import com.labijie.infra.orm.testing.tables.TestTable
import org.jetbrains.exposed.sql.insert
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.dao.DuplicateKeyException
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.transaction.support.TransactionTemplate
import kotlin.test.Test


@ExposedTest
@EnableAutoConfiguration
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [TestingContext::class])
class JdbcTester {


@Autowired
private lateinit var transactionTemplate: TransactionTemplate
@Test
fun exceptionTranslateTest() {

Assertions.assertThrowsExactly(DuplicateKeyException::class.java) {
this.transactionTemplate.execute {
TestTable.insert {
it[id] = 1
it[name1] = "name1"
it[name2] = "name2"
it[delete] = false
}

TestTable.insert {
it[id] = 1
it[name1] = "name1"
it[name2] = "name2"
it[delete] = false
}
}
}
}
}
Loading

0 comments on commit 7860774

Please sign in to comment.