Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I more easily use Jooq transactions in Kotlin

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.

like image 669
Jayson Minard Avatar asked May 09 '16 14:05

Jayson Minard


1 Answers

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:

  • create new transaction on call (although caller may not be aware this is happening)
  • only inherit existing transaction on call (enforced at compile time if only this version of the method is present)
  • inherit existing, if not create new transaction on call (enforced at compile time, correct version called when two are present)

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.

like image 168
3 revs Avatar answered Jan 03 '23 00:01

3 revs