jOOQ是如何设计事务API(详细指南)

从jOOQ 3.4开始,我们在jOOQ的JDBC之上有一个简化事务逻辑的API,从jOOQ 3.17和#13502开始,在R2DBC之上也将提供一个同等的API,用于反应式应用。

与所有的jOOQ一样,交易是使用显式的、基于API的逻辑实现的。在Jakarta EE和Spring中实现的隐式逻辑对于那些到处使用注解和方面的平台来说非常有效,但基于注解的范式并不适合jOOQ。

本文展示了jOOQ是如何设计事务API的,以及为什么SpringPropagation.NESTED 语义在jOOQ中是默认的。

遵循JDBC的默认值

在JDBC中(和R2DBC一样),一个独立的语句总是非交易性的,或者说是自动提交的。对jOOQ来说也是如此。如果你把一个非交易性的JDBC连接传递给jOOQ,像这样的查询也将是自动提交的:

ctx.insertInto(BOOK)
   .columns(BOOK.ID, BOOK.TITLE)
   .values(1, "Beginning jOOQ")
   .values(2, "jOOQ Masterclass")
   .execute();

到目前为止还不错,这在大多数API中都是一个合理的默认值。但通常,你不会自动提交。你写的是事务性逻辑。

事务性的lambdas

如果你想在一个事务中运行多个语句,你可以在jOOQ中这样写:

// The transaction() call wraps a transaction
ctx.transaction(trx -> {

    // The whole lambda expression is the transaction's content
    trx.dsl()
       .insertInto(AUTHOR)
       .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
       .values(1, "Tayo", "Koleoso")
       .values(2, "Anghel", "Leonard")
       .execute();

    trx.dsl()
       .insertInto(BOOK)
       .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
       .values(1, 1, "Beginning jOOQ")
       .values(2, 2, "jOOQ Masterclass")
       .execute();

    // If the lambda is completed normally, we commit
    // If there's an exception, we rollback
});

其心理模型与Jakarta EE和Spring@Transactional 方面完全相同。正常完成隐含地提交,特殊完成隐含地回滚。整个lambda是一个原子的 "工作单元",这是非常直观的。

你拥有你的控制流

如果你的代码中存在任何可恢复的异常,你可以优雅地处理它,而jOOQ的事务管理不会注意到。比如说:

ctx.transaction(trx -> {
    try {
        trx.dsl()
           .insertInto(AUTHOR)
           .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
           .values(1, "Tayo", "Koleoso")
           .values(2, "Anghel", "Leonard")
           .execute();
    }
    catch (DataAccessException e) {

        // Re-throw all non-constraint violation exceptions
        if (e.sqlStateClass() != C23_INTEGRITY_CONSTRAINT_VIOLATION)
            throw e;

        // Ignore if we already have the authors
    }

    // If we had a constraint violation above, we can continue our
    // work here. The transaction isn't rolled back
    trx.dsl()
       .insertInto(BOOK)
       .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
       .values(1, 1, "Beginning jOOQ")
       .values(2, 2, "jOOQ Masterclass")
       .execute();
});

在大多数其他API中也是如此,包括Spring。如果Spring不知道你的异常,它就不会将这些异常解释为事务性逻辑,这是很有意义的。毕竟,任何第三方库都可能在 没有注意到的情况下抛出和捕获内部异常,那么为什么Spring要注意呢。

事务传播

Jakarta EE和Spring提供了多种事务传播模式([TxType](https://jakarta.ee/specifications/platform/8/apidocs/javax/transaction/transactional.txtype)在Jakarta EE中。 [Propagation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html)在Spring中)。两者中默认的是REQUIRED 。我一直在努力研究为什么REQUIRED 是默认的,而不是NESTED ,我觉得这更符合逻辑和正确,我将在之后解释。如果你知道,请在微博或评论中告诉我:

为什么Propagation.REQUIRED是Spring的默认值?NESTED似乎是一个更好的默认值。

一个NESTED的事务单元是真正的事务。
REQUIRED事务性单元可能会让数据处于一种奇怪的状态,这取决于它是被顶层调用还是从嵌套的范围调用。

- Lukas Eder (@lukaseder)April 28, 2022

我对这些API的假设是:

  1. NESTED 需要 ,这在所有支持事务的RDBMS中都是不存在的。SAVEPOINT
  2. REQUIRED 避免了SAVEPOINT 的开销,如果你实际上不需要嵌套事务,这可能是一个问题(尽管我们可能会争论说,这样API就被错误地注释了太多的附带的 注释。就像@Transactional 你不应该无意识地运行 ,SELECT *你也不应该在没有充分考虑的情况下注解所有的东西)。
  3. 在Spring的用户代码中,每个 服务方法都只是盲目地注解了@Transactional ,而没有过多地考虑这个话题(和错误处理一样),然后,让事务REQUIRED ,而不是NESTED ,这只是一个更方便的默认 "让它工作"。这将有利于REQUIRED ,更像是一个偶然的默认值,而不是一个精心选择的默认值。
  4. JPA实际上不能很好地使用NESTED 事务,因为实体会被破坏(见Vlad对此的评论)。在我看来,这只是一个错误或缺失的功能,尽管我可以看到实现这个功能非常复杂,也许在JPA中不值得这样做。

所以,由于所有这些仅仅是技术上的原因,像Jakarta EE或Spring这样的API不把NESTED ,似乎是可以理解的(Jakarta EE甚至根本就不支持)。

但这是jOOQ,jOOQ一直在退一步思考事情应该如何发展,而不是对事情的现状印象深刻。

当你想一想下面的代码:

@Transactional
void tx() {
    tx1();

    try {
        tx2();
    }
    catch (Exception e) {
        log.info(e);
    }

    continueWorkOnTx1();
}

@Transactional
void tx1() { ... }

@Transactional
void tx2() { ... }

写这段代码的程序员的意图只能是一件事:

  • 启动一个全局事务,在tx()
  • 做一些嵌套的事务性工作,在tx1()
  • 尝试做一些其他嵌套的事务性工作,在tx2()
    • 如果tx2() 成功,很好,继续前进
    • 如果tx2() 失败,只需记录错误,ROLLBACKtx2() 之前,然后继续。
  • 不管tx2() ,继续用tx1()'s(也可能是tx2()'s)的结果工作。

但这不是REQUIRED ,它是Jakarta EE和Spring中默认的,会做什么。它将只是回滚tx2() tx1() ,让外部事务处于一个非常奇怪的状态,这意味着continueWorkOnTx1() 将会失败。但它真的应该失败吗?tx2() 应该是一个原子工作单元,与谁调用它无关。默认情况下,它不是这样的,所以Exception e 必须被传播。在catch 块中,在强制重新抛出之前,唯一可以做的事情是清理一些资源或做一些日志记录。(祝你好运,确保每个开发者都遵守这些规则!)

而且,一旦我们强制重新抛出,REQUIRED 就会变得和NESTED 一样,只是没有了保存点。所以,默认情况是:

  • 在快乐的道路上与NESTED 相同
  • 在不太高兴的情况下,就很奇怪了

这是支持将NESTED 作为默认值的有力论据,至少在jOOQ中是这样。现在,twitter上的讨论涉及到了很多架构方面的问题,比如为什么:

  • NESTED 是一个坏主意,或者在任何地方都行不通。
  • 悲观的锁是个坏主意
  • 等等

我并不反对其中的许多论点。然而, 关注列出的代码,并把自己放在一个库的开发者的位置上,程序员有可能通过这段代码达到什么目的?除了Spring的NESTED 事务语义之外,我看不出有什么别的东西。我实在看不出来。

jOOQ实现了NESTED语义

由于上述原因,如果支持保存点,jOOQ的事务只实现了Spring的NESTED 语义,如果不支持保存点,则完全不能嵌套(奇怪的是,这在Jakarta EE和Spring中都不是一个选项,因为这将是另一个合理的默认值)。与Spring的区别在于,所有的事情都是以编程方式明确完成的,而不是隐含地使用方面。

比如说:

ctx.transaction(trx -> {
    trx.dsl().transaction(trx1 -> {
        // ..
    });

    try {
        trx.dsl().transaction(trx2 -> {
            // ..
        });
    }
    catch (Exception e) {
        log.info(e);
    }

    continueWorkOnTrx1(trx);
});

如果trx2 出现异常而失败,只有trx2 被回滚。而不是trx1 。当然,你仍然可以重新抛出异常来回滚一切。但这里的立场是,如果你,程序员,告诉jOOQ运行一个嵌套事务,那么,jOOQ将服从,因为这是你想要的。

你不可能想要别的东西,因为那样的话,你就不会首先嵌套事务了,不是吗?

R2DBC事务

如前所述,jOOQ 3.17也将(最终)支持R2DBC的事务。其语义与JDBC的阻塞API完全相同,只是现在所有的东西都是Publisher 。所以,你现在可以写:

Flux<?> flux = Flux.from(ctx.transactionPublisher(trx -> Flux
    .from(trx.dsl()
        .insertInto(AUTHOR)
        .columns(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
        .values(1, "Tayo", "Koleoso")
        .values(2, "Anghel", "Leonard"))
    .thenMany(trx.dsl()
        .insertInto(BOOK)
        .columns(BOOK.ID, BOOK.AUTHOR_ID, BOOK.TITLE)
        .values(1, 1, "Beginning jOOQ")
        .values(2, 2, "jOOQ Masterclass"))
}));

这个例子使用reactor作为反应式流API的实现,但你也可以使用RxJava、Mutiny或其他什么。这个例子的工作原理和JDBC的完全一样,最初是这样的。

嵌套的工作方式也是一样的,以通常的、反应式的(也就是更费力的)方式:

Flux<?> flux = Flux.from(ctx.transactionPublisher(trx -> Flux
    .from(trx.dsl().transactionPublisher(trx1 -> { ... }))
    .thenMany(Flux
        .from(trx.dsl().transactionPublisher(trx2 -> { ... }))
        .onErrorContinue((e, t) -> log.info(e)))
    .thenMany(continueWorkOnTrx1(trx))
));

使用thenMany() 的排序只是一个例子。你可能会发现对完全不同的流构建基元的需求,这些基元与事务管理并无严格关系。

结论

嵌套事务偶尔也是有用的。在jOOQ中,事务传播比Jakarta EE或Spring要少得多,因为你所做的一切通常都是显式的,因此,你不会意外地嵌套事务,当你这样做时,你是故意的。这就是为什么jOOQ选择了与Spring不同的默认值,而且是Jakarta EE完全不支持的默认值。Propagation.NESTED 语义,这是一种强大的方式,可以将费力的保存点相关逻辑从你的代码中剔除。

猜你喜欢

转载自juejin.im/post/7126042312806760456
今日推荐