在JPA的存在下,如何在Spring Boot中正确设置JOOQ的交易管理?

发布于 2025-02-08 01:49:41 字数 7408 浏览 0 评论 0 原文

让我们从一个小的集成测试开始:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@AutoConfigureTestEntityManager
@Transactional(isolation = READ_UNCOMMITTED)
class JooqConfigIT) {
    companion object {
        const val testProjectId = 2021520
    }

    @Autowired
    lateinit var ctx: DSLContext

    @Test
    fun `dialect is set to postgres`() {
        assertThat(ctx.configuration().dialect()).isEqualTo(SQLDialect.POSTGRES)
    }

    @RepeatedTest(2, name = LONG_DISPLAY_NAME)
    fun `A - if jOOQ does not respect Spring transactions, either A or B should fail`() {
        ctx.insertInto(PROJECT)
            .columns(PROJECT.ID, PROJECT.NAME)
            .values(testProjectId, "Hakuna")
            .execute()
    }

    @RepeatedTest(2, name = LONG_DISPLAY_NAME)
    fun `B- if Spring does not respect jOOQ transactions, either A or B should fail`() {
        ctx.transaction { config ->
            DSL.using(config)
                .insertInto(PROJECT)
                .columns(PROJECT.ID, PROJECT.NAME)
                .values(testProjectId, "Matata")
                .execute()
        }
    }
}

前提很简单:

  • 每个 @test 在测试结束时都会回滚,因此,
  • 如果正确集成不会有例外,因为 testProjectID 在任一测试结束时都不会在数据库中。另一方面,
  • 如果未正确设置,则至少一个 a b 将其写入数据库中,并且至少一个重复将失败,因为它是使用相同的ID,
  • 我们正在重复 a b 的原因是,如果正确设置了任一方向并且另一个方向不是 (例如Spring知道JOOQ交易,但JOOQ不知道春季交易),我们仍然永远无法遇到我们在所有测试结束时进行交易的情况(因此不要捕获错误),因为在写作后对于数据库,仍然有第二次测试试图做同样的事情并会失败,

以便我们知道我们想要什么,但是我们如何到达那里,

网络上有多个资源告诉您如何实现这一目标,让我保存您谷歌搜索并在这里提到它:

@Configuration
class JooqConfig(
    private val dataSource: DataSource,
    private val transactionManager: DataSourceTransactionManager,
) {

    /** Makes jOOQ respect Spring transactions (e.g. [Transactional] annotations)*/
    @Bean
    fun dataSourceConnectionProvider() = DataSourceConnectionProvider(TransactionAwareDataSourceProxy(dataSource))

    /** Makes Spring respect jOOQ transactions (i.e. `ctx.transaction{config -> ...}`)*/
    @Bean
    fun springTransactionProvider(): TransactionProvider = SpringTransactionProvider(transactionManager)

    private class SpringTransactionProvider(private val transactionManager: DataSourceTransactionManager) : TransactionProvider {
        companion object {
            private val log: JooqLogger = JooqLogger.getLogger(SpringTransactionProvider::class.java)
        }

        override fun begin(ctx: TransactionContext) {
            log.info("begin transaction")
            val transactionDefinition = DefaultTransactionDefinition(PROPAGATION_NESTED)
            val transactionStatus = transactionManager.getTransaction(transactionDefinition)
            ctx.transaction(SpringTransaction(transactionStatus))
        }

        override fun commit(ctx: TransactionContext) {
            log.info("commit transaction");
            transactionManager.commit(ctx.transactionStatus)
        }

        override fun rollback(ctx: TransactionContext) {
            log.info("rollback transaction");
            transactionManager.rollback(ctx.transactionStatus)
        }

        private class SpringTransaction(val status: TransactionStatus) : Transaction

        private val TransactionContext.transactionStatus
            get() = when (val tx = this.transaction()) {
                is SpringTransaction -> tx.status
                else -> throw IllegalStateException("Expected SpringTransaction but was $tx")
            }
    }
}

但是,我尚未找到尚未发现的任何地方,春季启动已经在 org.springframework.boot.boot.autoconfigure.jooq中完全做到了。 jooqautoconfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DSLContext.class)
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class })
public class JooqAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(ConnectionProvider.class)
    public DataSourceConnectionProvider dataSourceConnectionProvider(DataSource dataSource) {
        return new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(dataSource));
    }

    @Bean
    @ConditionalOnBean(PlatformTransactionManager.class)
    public SpringTransactionProvider transactionProvider(PlatformTransactionManager txManager) {
        return new SpringTransactionProvider(txManager);
    }

    //...

}

因此,这都是不需要的,除非您是像我这样的幸运者之一,他们碰巧在应用程序中也有JPA,因为您正在考虑迁移到JOOQ。 (或在我的情况下,真的很想最终摆​​脱JPA),在这种情况下, b 可能会失败,但不是因为Spring不了解交易,而是因为

JpaDialect does not support savepoints - check your JPA provider's capabilities
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
    at org.springframework.orm.jpa.JpaTransactionManager$JpaTransactionObject.getSavepointManager(JpaTransactionManager.java:762)
    at org.springframework.orm.jpa.JpaTransactionManager$JpaTransactionObject.createSavepoint(JpaTransactionManager.java:741)
    at org.springframework.transaction.support.AbstractTransactionStatus.createAndHoldSavepoint(AbstractTransactionStatus.java:140)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:457)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:352)
    at org.springframework.boot.autoconfigure.jooq.SpringTransactionProvider.begin(SpringTransactionProvider.java:48)
    at org.jooq.impl.DefaultDSLContext.lambda$transactionResult0$0(DefaultDSLContext.java:537)
    at org.jooq.impl.Tools$35$1.block(Tools.java:5246)
    at java.base/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3128)
    at org.jooq.impl.Tools$35.get(Tools.java:5243)
    at org.jooq.impl.DefaultDSLContext.transactionResult0(DefaultDSLContext.java:595)
    at org.jooq.impl.DefaultDSLContext.transactionResult(DefaultDSLContext.java:512)
    at org.jooq.impl.DefaultDSLContext.transaction(DefaultDSLContext.java:612)
    at my.app.config.JooqConfigIT.B- if Spring does not respect jOOQ transactions, either A or B should fail(JooqConfigIT.kt:36)
    at ...

我们学习了三个JPA很烂

  • - 但是我已经知道,
  • 即使您放入自己的 JOOQCONFIG 定义此不错的 springtransaction provider bean时取而代之的是使用自己的(从StackTrace中可以明显看出),
  • 我真的不在乎嵌套交易,因为我要创建自己的 @repository 类,无论如何,这些类都会整齐地隐藏在之后@transactional 宣布服务,因此所有交易管理将在春季之前得到照顾。

i 可以只需按@transactional 从类宣传到 a 测试方法,在 b中实现手动回滚(异常) 并将其称为一天。

或者,我们可以花时间尝试提出适当的配置。

我的想法是只定义自己的 springtransaction -provider ,将 TransActionDefinition 更改为 propagation_required st jooq st jooq不尝试创建嵌套交易并希望修复修复事物。

但是,我的豆根本没有出现在堆叠式的情况下,这一事实使我认为这不是要走的路,应该有不同的方式。

那么我该如何完成这项工作?

Let's start with a small integration test:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@AutoConfigureTestEntityManager
@Transactional(isolation = READ_UNCOMMITTED)
class JooqConfigIT) {
    companion object {
        const val testProjectId = 2021520
    }

    @Autowired
    lateinit var ctx: DSLContext

    @Test
    fun `dialect is set to postgres`() {
        assertThat(ctx.configuration().dialect()).isEqualTo(SQLDialect.POSTGRES)
    }

    @RepeatedTest(2, name = LONG_DISPLAY_NAME)
    fun `A - if jOOQ does not respect Spring transactions, either A or B should fail`() {
        ctx.insertInto(PROJECT)
            .columns(PROJECT.ID, PROJECT.NAME)
            .values(testProjectId, "Hakuna")
            .execute()
    }

    @RepeatedTest(2, name = LONG_DISPLAY_NAME)
    fun `B- if Spring does not respect jOOQ transactions, either A or B should fail`() {
        ctx.transaction { config ->
            DSL.using(config)
                .insertInto(PROJECT)
                .columns(PROJECT.ID, PROJECT.NAME)
                .values(testProjectId, "Matata")
                .execute()
        }
    }
}

Premise is simple:

  • there will be a transaction at the start of each @Test that will be rolled back at the end of the test, so
  • if jOOQ is integrated correctly, there will be no exception because testProjectId will not be in the database by the end of either test. on the other hand,
  • if it's not set up correctly, at least one of A or B will write it into the database and at least one of the repetitions will fail because it's using the same ID
  • the reason we're duplicating both A and B is that if either direction IS set up correctly and the other isn't
    (e.g. Spring is aware of jOOQ transactions but jOOQ is not aware of Spring transactions), we still can never run into the case where we commit the transaction at the end of all tests (and therefore don't catch the error) because after writing to the DB, there's still a second test that tries to do the same thing and will fail

So we know what we want, but how do we get there

There are multiple resources on the web telling you how to achieve that, let me save you the googling and just mention it here:

@Configuration
class JooqConfig(
    private val dataSource: DataSource,
    private val transactionManager: DataSourceTransactionManager,
) {

    /** Makes jOOQ respect Spring transactions (e.g. [Transactional] annotations)*/
    @Bean
    fun dataSourceConnectionProvider() = DataSourceConnectionProvider(TransactionAwareDataSourceProxy(dataSource))

    /** Makes Spring respect jOOQ transactions (i.e. `ctx.transaction{config -> ...}`)*/
    @Bean
    fun springTransactionProvider(): TransactionProvider = SpringTransactionProvider(transactionManager)

    private class SpringTransactionProvider(private val transactionManager: DataSourceTransactionManager) : TransactionProvider {
        companion object {
            private val log: JooqLogger = JooqLogger.getLogger(SpringTransactionProvider::class.java)
        }

        override fun begin(ctx: TransactionContext) {
            log.info("begin transaction")
            val transactionDefinition = DefaultTransactionDefinition(PROPAGATION_NESTED)
            val transactionStatus = transactionManager.getTransaction(transactionDefinition)
            ctx.transaction(SpringTransaction(transactionStatus))
        }

        override fun commit(ctx: TransactionContext) {
            log.info("commit transaction");
            transactionManager.commit(ctx.transactionStatus)
        }

        override fun rollback(ctx: TransactionContext) {
            log.info("rollback transaction");
            transactionManager.rollback(ctx.transactionStatus)
        }

        private class SpringTransaction(val status: TransactionStatus) : Transaction

        private val TransactionContext.transactionStatus
            get() = when (val tx = this.transaction()) {
                is SpringTransaction -> tx.status
                else -> throw IllegalStateException("Expected SpringTransaction but was $tx")
            }
    }
}

What I haven't yet found, though, is a mention anywhere that Spring Boot already does exactly that in org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DSLContext.class)
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class })
public class JooqAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(ConnectionProvider.class)
    public DataSourceConnectionProvider dataSourceConnectionProvider(DataSource dataSource) {
        return new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(dataSource));
    }

    @Bean
    @ConditionalOnBean(PlatformTransactionManager.class)
    public SpringTransactionProvider transactionProvider(PlatformTransactionManager txManager) {
        return new SpringTransactionProvider(txManager);
    }

    //...

}

So none of this is necessary, EXCEPT if you're one of the lucky people like me who happen to also have JPA in the app because you're thinking about migrating to jOOQ. (Or in my case, really eager to eventually get rid of JPA), in which case B may fail for you, but not because Spring isn't aware of the transactions, but rather because

JpaDialect does not support savepoints - check your JPA provider's capabilities
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
    at org.springframework.orm.jpa.JpaTransactionManager$JpaTransactionObject.getSavepointManager(JpaTransactionManager.java:762)
    at org.springframework.orm.jpa.JpaTransactionManager$JpaTransactionObject.createSavepoint(JpaTransactionManager.java:741)
    at org.springframework.transaction.support.AbstractTransactionStatus.createAndHoldSavepoint(AbstractTransactionStatus.java:140)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:457)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:352)
    at org.springframework.boot.autoconfigure.jooq.SpringTransactionProvider.begin(SpringTransactionProvider.java:48)
    at org.jooq.impl.DefaultDSLContext.lambda$transactionResult0$0(DefaultDSLContext.java:537)
    at org.jooq.impl.Tools$35$1.block(Tools.java:5246)
    at java.base/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3128)
    at org.jooq.impl.Tools$35.get(Tools.java:5243)
    at org.jooq.impl.DefaultDSLContext.transactionResult0(DefaultDSLContext.java:595)
    at org.jooq.impl.DefaultDSLContext.transactionResult(DefaultDSLContext.java:512)
    at org.jooq.impl.DefaultDSLContext.transaction(DefaultDSLContext.java:612)
    at my.app.config.JooqConfigIT.B- if Spring does not respect jOOQ transactions, either A or B should fail(JooqConfigIT.kt:36)
    at ...

We learn three things from this

  • JPA sucks - but I already knew that when I started dabbling with jOOQ
  • even if you put in your own JooqConfig that defines this nice springTransactionProvider bean, Spring ignores it and instead uses its own (as evident from the stacktrace)
  • I really don't care about nested transactions as I'm going to create my own @Repository classes anyway that will be neatly hidden behind an @Transactional-annotated service so all the transaction management will already be taken care of by Spring.

I could just push the @Transactional annotation from the class to the A test method, implement a manual rollback (exception) in B and call it a day.

OR, we could take the time to try to come up with a proper configuration.

My idea was to just define my own springTransactionProvider, change the TransactionDefinition to PROPAGATION_REQUIRED s.t. jOOQ does not try to create a nested transaction and hope that fixes things.

But the fact that my bean does not show up in the stacktrace at all makes me think this is not the way to go and there should be a different way.

So how do I make this work?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文