我怎样才能更轻松地在 Kotlin 中使用 Jooq 事务

How can I more easily use Jooq transactions in Kotlin

我有使用事务在 Kotlin 中编写的 Jooq 代码,有时我希望一个方法作为顶级操作独立工作,它将有自己的事务,而其他时候希望它与其他方法组合工作在同一笔交易中。例如,我有两个较低级别的函数 actionAbcactionXyz,我想将它们组合成不同的较高级别数据方法并继承它们的事务(如果存在),否则有它们自己的。

我知道在 Spring 或其他框架中可以添加注释来验证“需要交易”或“如果 none 则创建交易”类型的功能。但是我如何在不使用这些库的情况下对 Jooq + Kotlin 做同样的事情呢?

我想到的最接近的方法是将交易作为可选参数传入,如果丢失则默认为新交易。但是如果有人忘记传递交易,那么使用新的顶级和不相关的交易就会出现微妙的失败,我不希望那样。

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
    }
  }
}

如何更自然、更安全地执行此操作?

注:这个问题是作者(Self-Answered Questions)特意写的,所以常见的Kotlin话题的答案是出现在 SO.

为了解决这个问题,您可以使用扩展函数让一些方法只在一个事务中可用。首先,我们修复交易功能,使其有两种风格,一种是顶级交易,一种是嵌套交易。

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
}

现在您的事务将无缝嵌套,绝不会出错。因为 Kotlin 在用作嵌套事务时会优先选择更具体的扩展函数。

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
         ...
       }
     }
   }
}

现在可以将相同的原则应用于方法 actionAbcactionXyz,以便它们只能从事务中调用。

fun DSLContext.actionAbc(parm1: String, parm2: Int) {
   ...
}

fun DSLContext.actionXyz(parm: Date) {
   ...
}

他们不再创建交易,因为他们保证只能从一个内部调用。它们现在的用途自然是:

fun higherLevelAction(parm1: String, parm2: Date) {
  tx {
    actionAbc(parm1, 45)
    actionXyz(parm2)

    tx {
       // nesting naturally
       ...
    }
  }
}

没有事务就无法调用actionAbcactionXyz。因此,如果你想让它们具有双重用途,我们可以创建第二种风格的操作,它创建自己的事务并委托给另一个。例如 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
}

现在 actionAbc 可以独立调用,也可以在另一个事务中调用,编译器将根据接收者决定调用哪个版本。

唯一需要注意的是,如果这些是 class 方法,那么它们只能在同一个 class 中调用,因为您不能同时指定一个实例和一个接收者来调用一种方法。

上面的例子涵盖了这些情况:

  • 随叫随到创建新事务(尽管调用者可能不知道这正在发生)
  • 仅继承调用时的现有事务(如果仅存在此版本的方法,则在编译时强制执行)
  • 继承现有的,如果不创建新的调用事务(在编译时强制执行,当两个存在时调用正确的版本)

如果要拒绝在已经存在事务的情况下调用方法的情况,只需实现扩展版本并抛出异常:

@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 { 
      ...
   } 
}

由于使用了级别设置为 ERROR@Deprecation 注释,编译器将检查最后一个案例。您还可以允许调用,并委托给其他方法并将弃用设置为 WARNING 级别,以便用户意识到潜在的问题,但也可以使用 @Suppress("DEPRECATION") 来抑制警告调用语句。