Skip to content

Commit

Permalink
Support for Spring @transactional
Browse files Browse the repository at this point in the history
ExposedTransactionManager added
  • Loading branch information
Tapac committed Apr 15, 2016
1 parent e2c36b6 commit ad4a69d
Show file tree
Hide file tree
Showing 12 changed files with 457 additions and 248 deletions.
49 changes: 37 additions & 12 deletions build.gradle
Expand Up @@ -41,22 +41,11 @@ repositories {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
url "http://dl.bintray.com/kotlin/kotlin"
}
}

dependencies {
compile 'joda-time:joda-time:2.5'
compile 'org.slf4j:slf4j-api:1.7.12'

testCompile 'junit:junit:4.11'
compile 'com.h2database:h2:1.4.186' // Shall be scope 'provided' instead

compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testCompile 'org.slf4j:slf4j-log4j12:1.7.12'
testCompile 'log4j:log4j:1.2.17'
testCompile "mysql:mysql-connector-mxj:5.0.12"
testCompile 'org.postgresql:postgresql:9.4.1208.jre7'
testCompile 'ru.yandex.qatools.embed:postgresql-embedded:1.10'
Expand Down Expand Up @@ -93,3 +82,39 @@ afterReleaseBuild.dependsOn bintrayUpload
task wrapper(type: Wrapper) {
gradleVersion = "2.7"
}


allprojects {
apply plugin: 'kotlin'
apply plugin: 'maven-publish'

repositories {
mavenCentral()
}

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile 'joda-time:joda-time:2.5'
compile 'org.slf4j:slf4j-api:1.7.12'

testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testCompile 'org.slf4j:slf4j-log4j12:1.7.12'
testCompile 'log4j:log4j:1.2.17'
testCompile 'junit:junit:4.11'
testCompile 'com.h2database:h2:1.4.186'

}
}


project(":spring-transaction") {
apply plugin: 'kotlin'


dependencies {
compile 'org.springframework:spring-jdbc:4.2.5.RELEASE'
compile 'org.springframework:spring-context:4.2.5.RELEASE'
compile rootProject
}
}
4 changes: 3 additions & 1 deletion settings.gradle
@@ -1 +1,3 @@
rootProject.name='exposed'
rootProject.name='exposed'
include 'spring-transaction'

@@ -0,0 +1,61 @@
package org.jetbrains.exposed.spring

import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.TransactionAbstraction
import org.jetbrains.exposed.sql.TransactionProvider
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.DefaultTransactionDefinition
import org.springframework.transaction.support.DefaultTransactionStatus
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.sql.Connection
import javax.sql.DataSource


class ExposedTransactionManager(dataSource: DataSource) : DataSourceTransactionManager(dataSource), TransactionProvider {

private val db = Database.connect(dataSource, { this } )

override fun newTransaction(isolation: Int): Transaction {
val tDefinition = if (dataSource.connection.transactionIsolation != isolation) {
DefaultTransactionDefinition().apply { isolationLevel = isolation }
} else null
val tObject = (getTransaction(tDefinition) as DefaultTransactionStatus).transaction as JdbcTransactionObjectSupport
return Transaction(SpringTransaction(tObject, db)).apply {
TransactionSynchronizationManager.bindResource(this@ExposedTransactionManager, this)
}
}

override fun close() {
TransactionSynchronizationManager.unbindResourceIfPossible(this)
}

override fun currentOrNull(): Transaction? {
return TransactionSynchronizationManager.getResource(this) as Transaction? ?: newTransaction(TransactionDefinition.ISOLATION_DEFAULT)
}

private class SpringTransaction(private val tObject: JdbcTransactionObjectSupport, override val db: Database) : TransactionAbstraction {

override val connection: Connection get() = tObject.connectionHolder.connection

override fun commit() {
tObject.connectionHolder.connection.run {
if (!autoCommit) {
commit()
}
}
}

override fun rollback() {
tObject.connectionHolder.connection.rollback()
}

override fun close() {
tObject.connectionHolder.connection.close()
}

}

}
@@ -0,0 +1,63 @@
package org.jetbrains.exposed.spring

import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.junit.Assert
import org.junit.Test
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import org.springframework.transaction.annotation.TransactionManagementConfigurer
import org.springframework.transaction.annotation.Transactional
import javax.sql.DataSource


private val ctx = AnnotationConfigApplicationContext(TestConfig::class.java)

@Transactional
class ExposedTransactionManagerTest {

@Test
fun testConnection() {

try {
val pm = ctx.getBean(PlatformTransactionManager::class.java)
if(pm !is ExposedTransactionManager) error("Wrong txManager instance: ${pm.javaClass.name}")

val t1 = object : Table() {
val c1 = varchar("c1", 5).nullable()
}

SchemaUtils.create(t1)
t1.insert {
it[c1] = "112"
}

Assert.assertEquals(t1.selectAll().count(), 1)

} finally {
(ctx.getBean(DataSource::class.java) as EmbeddedDatabase).shutdown()
}

}

}

@Configuration
@EnableTransactionManagement
open class TestConfig : TransactionManagementConfigurer {

@Bean
open fun ds() = EmbeddedDatabaseBuilder().setName("embeddedTest").setType(EmbeddedDatabaseType.H2).build()

@Bean
override fun annotationDrivenTransactionManager(): PlatformTransactionManager? = ExposedTransactionManager(ds())

}
63 changes: 7 additions & 56 deletions src/main/kotlin/org/jetbrains/exposed/sql/Database.kt
Expand Up @@ -7,7 +7,6 @@ import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect
import java.sql.Connection
import java.sql.DatabaseMetaData
import java.sql.DriverManager
import java.sql.SQLException
import java.util.concurrent.CopyOnWriteArrayList
import javax.sql.DataSource

Expand All @@ -32,55 +31,6 @@ class Database private constructor(val connector: () -> Connection) {
val vendor: String get() = dialect.name


// Overloading methods instead of default parameters for Java compatibility
fun <T> transaction(statement: Transaction.() -> T): T = transaction(Connection.TRANSACTION_REPEATABLE_READ, 3, statement)

fun <T> transaction(transactionIsolation: Int, repetitionAttempts: Int, statement: Transaction.() -> T): T {
val outer = Transaction.currentOrNull()

return if (outer != null) {
outer.statement()
}
else {
inTopLevelTransaction(transactionIsolation, repetitionAttempts, statement)
}
}

fun <T> inTopLevelTransaction(transactionIsolation: Int, repetitionAttempts: Int, statement: Transaction.() -> T): T {
var repetitions = 0

while (true) {

val transaction = Transaction(this, {
val connection = connector()
connection.autoCommit = false
connection.transactionIsolation = transactionIsolation
connection
})

try {
val answer = transaction.statement()
transaction.commit()
return answer
}
catch (e: SQLException) {
exposedLogger.info("Transaction attempt #$repetitions: ${e.message}", e)
transaction.rollback()
repetitions++
if (repetitions >= repetitionAttempts) {
throw e
}
}
catch (e: Throwable) {
transaction.rollback()
throw e
}
finally {
transaction.close()
}
}
}

companion object {
private val dialects = CopyOnWriteArrayList<DatabaseDialect>()

Expand All @@ -94,17 +44,18 @@ class Database private constructor(val connector: () -> Connection) {
dialects.add(0, dialect)
}

fun connect(datasource: DataSource): Database {
return Database {
datasource.connection!!
fun connect(datasource: DataSource, provider: (Database) -> TransactionProvider = { ThreadLocalTransactionProvider(it) }): Database {
return Database { datasource.connection!! }.apply {
TransactionProvider.provider = provider(this)
}
}

fun connect(url: String, driver: String, user: String = "", password: String = ""): Database {
fun connect(url: String, driver: String, user: String = "", password: String = "",
provider: (Database) -> TransactionProvider = { ThreadLocalTransactionProvider(it) }): Database {
Class.forName(driver).newInstance()

return Database {
DriverManager.getConnection(url, user, password)
return Database { DriverManager.getConnection(url, user, password) }.apply {
TransactionProvider.provider = provider(this)
}
}
}
Expand Down

0 comments on commit ad4a69d

Please sign in to comment.