diff --git a/README.md b/README.md index 0a86eda..a36e647 100644 --- a/README.md +++ b/README.md @@ -589,7 +589,7 @@ At the end of the withAutoCommit block, the AutoCommit ConnectionSession will be ## withTransaction By using a Transaction ConnectionSession, changes will NOT be immediately committed to the database. Which allows for -multiple features listed below. If any of these features are required, use withTransaction. +multiple features listed below. If any of these features are required, use withTransaction. Also use withTransaction if you need to specify isolation level. * Commit - Commits any existing changes to the database and clears any Savepoints and Locks * Rollback - Reverts the changes since the most recent commit, or the beginning of the ConnectionSession if no commits @@ -603,6 +603,18 @@ At the end of the withTransaction block, if the block is exited normally the Tra exception is thrown, the Transaction will be rolled back. After the final commit/rollback, the Transaction ConnectionSession will be closed. +### withTransaction - How to Specify Isolation levels + +By default, all transactions run with `TRANSACTION_READ_COMMITTED`isolation level. The following shows how to specify a higher one: + +```kotlin + db.withTransaction(isolationLevel = Db.IsolationLevel.TRANSACTION_REPEATABLE_READ) + + db.withTransaction(isolationLevel = Db.IsolationLevel.TRANSACTION_SERIALIZABLE) +``` + +When the transaction is over, isolation level is restored to the default, TRANSACTION_READ_COMMITTED. + ## DataSource configuration & AutoCommit A dataSource has a default setting for the autocommit flag which can be configured. But the individual connections can diff --git a/src/main/kotlin/com/target/liteforjdbc/Db.kt b/src/main/kotlin/com/target/liteforjdbc/Db.kt index c1b1596..a3448f5 100644 --- a/src/main/kotlin/com/target/liteforjdbc/Db.kt +++ b/src/main/kotlin/com/target/liteforjdbc/Db.kt @@ -118,20 +118,34 @@ open class Db( * or rolls back if it throws an exception. This is required to perform any DB interactions that need transaction * support. */ - fun withTransaction(block: (Transaction) -> T): T { + fun withTransaction( + isolationLevel: IsolationLevel, + block: (Transaction) -> T + ): T { val transaction = Transaction(connection = dataSource.connection) - transaction.use { + val currentIsolationLevel = transaction.connection.transactionIsolation + return transaction.use { try { + transaction.connection.transactionIsolation = isolationLevel.intCode val result = block(transaction) transaction.commit() - return result + result } catch (t: Throwable) { transaction.rollback() throw t + } finally { + transaction.connection.transactionIsolation = currentIsolationLevel } } } + /** + * Uses a com.target.liteforjdbc.Transaction, and commits it once to the block is executed successfully, + * or rolls back if it throws an exception. This is required to perform any DB interactions that need transaction + * support. + */ + fun withTransaction(block: (Transaction) -> T): T = withTransaction(IsolationLevel.TRANSACTION_READ_COMMITTED, block) + /** * Uses a com.target.liteforjdbc.AutoCommit and closes it once teh block is executed. This can be useful to use a * single connection from the DataSource for a series of actions. Using other convenience query methods on this @@ -169,5 +183,11 @@ open class Db( */ fun isDataSourceHealthy() = useConnection { !it.isClosed } + enum class IsolationLevel(val intCode: Int) { + TRANSACTION_READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED), + TRANSACTION_REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), + TRANSACTION_SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE) + } + } diff --git a/src/test/kotlin/com/target/liteforjdbc/postgres/PostgresSqlIntegrationTest.kt b/src/test/kotlin/com/target/liteforjdbc/postgres/PostgresSqlIntegrationTest.kt index 76c8d79..196e15f 100644 --- a/src/test/kotlin/com/target/liteforjdbc/postgres/PostgresSqlIntegrationTest.kt +++ b/src/test/kotlin/com/target/liteforjdbc/postgres/PostgresSqlIntegrationTest.kt @@ -441,6 +441,34 @@ class PostgresSqlIntegrationTest { finalCount shouldBe originalCount } + + @Test + fun setsReadCommittedIsolationLevel() { + checkIsolationLevel(Db.IsolationLevel.TRANSACTION_READ_COMMITTED, "read committed") + } + + @Test + fun setsRepeatableReadIsolationLevel() { + checkIsolationLevel(Db.IsolationLevel.TRANSACTION_REPEATABLE_READ, "repeatable read") + } + + @Test + fun setsSerializatableIsolationLevel() { + checkIsolationLevel(Db.IsolationLevel.TRANSACTION_SERIALIZABLE, "serializable") + } + + private fun checkIsolationLevel(isolationLevel: Db.IsolationLevel, expected: String) { + val level = db.withTransaction(isolationLevel) { transaction -> + checkNotNull( + transaction.executeQuery( + "SELECT current_setting('transaction_isolation')" + ) { resultSet: ResultSet -> + resultSet.getString("current_setting") + } + ) + } + level shouldBe expected + } private fun tableCount(): Int { return checkNotNull(