I have Jooq code written in Kotlin using transactions, and sometimes I want a method to work stand-alone as a top-level action that will have its own transactions, and other times want it to work composited with other methods within the same transaction. For example, I have two lower level functions actionAbc
and actionXyz
that I want to compose into different higher level data methods and inherit their transaction if present, otherwise have their own.
I know in Spring or other frameworks there are annotations that can be added that validate "transaction required" or "create transaction if none" type functionality. But how do I do the same with Jooq + Kotlin without using those libraries?
The closest I came up with is to pass the transaction in as an optional parameter and default it to a new transaction if missing. But if someone forgets to pass in the transaction, then has the subtle failure of using a new top-level and unrelated transaction, I don't want that.
fun tx(ctx: DSLContext = rootContext, codeBlock: DSLContext.() -> Unit): Unit {
ctx.transaction { cfg ->
DSL.using(cfg).codeBlock()
}
}
}
// and used as:
fun actionAbc(parm1: String, parm2: Int, ctx: DSLContext = rootContext) {
tx(ctx) { ... }
}
fun actionXyz(parm: Date, ctx: DSLContext = rootContext) {
tx(ctx) { ... }
}
// composed:
fun higherLevelAction(parm1: String, parm2: Date) {
tx {
actionAbc(parm1, 45, this) // if you forget `this` you are doing the wrong thing
actionXyz(parm2, this)
tx(this) {
// nested transaction, also dangerous if forgetting `this` parameter
}
}
}
How do I do this more naturally, and less dangerously?
Note: this question is intentionally written and answered by the author (Self-Answered Questions), so that the answers to commonly asked Kotlin topics are present in SO.
To solve this, you can use extension functions to make some methods only available within a transaction. First, we fix the transaction function so that there are two flavours, one that is top level, and one that is a nested transaction.
fun <T : Any?> tx(codeBlock: DSLContext.() -> T): T {
return rootContext.txWithReturn(codeBlock)
}
fun <T : Any?> DSLContext.tx(codeBlock: DSLContext.() -> T): T {
var returnVal: T? = null
this.transaction { cfg ->
returnVal = DSL.using(cfg).codeBlock()
}
return returnVal as T
}
Now your transactions will nest seamlessly and never have the chance of error. Because Kotlin will pick the more specific extension function first when used as a nested transaction.
fun foo() {
tx { // calls the outer function that creates a transaction
...
tx { // calls the extension on DSLContext because our code block has receiver of DSLContext
...
tx { // calls the extension function, further nesting correctly
...
}
}
}
}
Now the same principle can be applied to the methods actionAbc
and actionXyz
so that they are only able to be called from within a transaction.
fun DSLContext.actionAbc(parm1: String, parm2: Int) {
...
}
fun DSLContext.actionXyz(parm: Date) {
...
}
They no longer create transactions because they are guaranteed to only be called from within one. Their use is now naturally:
fun higherLevelAction(parm1: String, parm2: Date) {
tx {
actionAbc(parm1, 45)
actionXyz(parm2)
tx {
// nesting naturally
...
}
}
}
It is impossible to call actionAbc
or actionXyz
without a transaction. So if you want to make them dual use, we can create a second flavour of the action that creates its own transaction and delegates to the other. For example for actionAbc
:
fun DSLContext.actionAbc(parm1: String, parm2: Int) {
...
}
fun actionAbc(parm1: String, parm2: Int) {
tx { actionAbc(parm1, parm2) } // delegates to one above but with a new transaction
}
Now actionAbc
is callable independently and also within another transaction and the compiler will decide based on the receiver which version to call.
The only caveat is that if these are class methods then they can only be called from within the same class since you cannot specify both an instance and a receiver at the same time for calling a method.
The examples above covers these cases:
If you want to reject the case where a method is called when there is already an existing transaction, just implement the extension version and throw an exception:
@Deprecated("Only call these without an existing transaction!",
level = DeprecationLevel.ERROR)
fun DSLContext.actionAbc(parm1: String, parm2: Int) {
throw IllegalStateException("Only call these without an existing transaction!")
}
fun actionAbc(parm1: String, parm2: Int) {
tx {
...
}
}
This last case will be checked by the compiler because of the use of @Deprecation
annotation with level set to ERROR
. You could also allow the call, and delegate to the other method and set the deprecation to WARNING
level so that the user is aware of the potential for a problem but can also suppress the warning using @Suppress("DEPRECATION")
on the calling statement.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With