从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 语义,这是一种强大的方法,可以将费劲的保存点相关逻辑从你的代码中除去。