尽管存在`@Transactional`,为啥这些数据库修改不回滚?

Posted

技术标签:

【中文标题】尽管存在`@Transactional`,为啥这些数据库修改不回滚?【英文标题】:Why aren't these database modifications rolled back despite the presence of an `@Transactional`?尽管存在`@Transactional`,为什么这些数据库修改不回滚? 【发布时间】:2019-09-02 05:17:24 【问题描述】:

Testcontainers写了一个简短的便利扩展:

fun JdbcDatabaseContainer<*>.execute(query:DSLContext.()-> Query)
    val connection = DriverManager.getConnection(this.getJdbcUrl(),this.getUsername(),this.getPassword())
    val create = DSL.using(connection)
    create.query().execute()

现在想测试一下。

Flyway 加载 30 个条目。这些应该在allDataPresent 中可见 canInsert 插入一个不带扩展名的条目 canInsertWithExtension 做同样的事情,但通过扩展功能 insertMultipleWithExtension 就像它的名字所暗示的那样,插入另一个 5

除了allDataPresent 测试用例(因为它是只读的)之外的所有测试用例都被注释@Transactional

因此,我希望在测试方法之后回滚这些修改。

反而会发生

[ERROR] Failures: 
[ERROR]   InitDataIT.allDataPresent:70 
Expecting:
 <36>
to be equal to:
 <30>
but was not.
[ERROR]   InitDataIT.canInsert:90 
Expecting:
 <6>
to be equal to:
 <1>
but was not.
[ERROR]   InitDataIT.canInsertWithExtension:112 
Expecting:
 <6>
to be equal to:
 <1>
but was not.

每个@Test 都可以自己正常工作。所以问题一定出在@Transactional

那是为什么呢?更重要的是,我如何获得回滚?

完整的测试用例(也尝试注释类,没有任何区别):

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = [InitDataIT.TestContextInitializer::class])
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
open class InitDataIT 
    companion object 
        @JvmStatic
        @Container
        private val dbContainer = mysqlContainer<Nothing>().apply 
            withDatabaseName("test")
            withUsername("root")
            withPassword("")
        
    
    object TestContextInitializer: ApplicationContextInitializer<ConfigurableApplicationContext> 
        override fun initialize(applicationContext: ConfigurableApplicationContext) 
            TestPropertyValues.of(
                    "spring.datasource.url=$dbContainer.jdbcUrl",
                    "spring.datasource.username=$dbContainer.username",
                    "spring.datasource.password=$dbContainer.password",
                    "spring.datasource.driver-class-name=$dbContainer.driverClassName"
            ).applyTo(applicationContext)
        
    

    private val create:DSLContext


    @Autowired
    constructor(create:DSLContext)
        this.create = create
    


    @Test
    fun allDataPresent()
        //given
        val expectedNumberOfEntries = 30

        val query = create.selectCount()
                .from(CUSTOMERS)

        //when
        val numberOfEntries = query.fetchOneit.value1()

        //then
        Assertions.assertThat(numberOfEntries).isEqualTo(expectedNumberOfEntries)
    

    @Test
    @Transactional
    open fun canInsert()
        //given
        val insertquery = create.insertInto(CUSTOMERS)
                .columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
                .values("Alice","Tester","Alice.Tester@somewhere.tt",CustomerStatus.Contacted.name)

        val expectedNumberInOffice2 = 1

        //when
        insertquery.execute()

        //then
        val numberInOffice2 = create.selectCount()
                .from(CUSTOMERS)
                .where(CUSTOMERS.EMAIL.contains("somewhere"))
                .fetchOneit.value1()
        assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)

    

    @Test
    @Transactional
    open fun canInsertWithExtension()
        //given
        dbContainer.execute 
            insertInto(CUSTOMERS)
                    .columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
                    .values("Alice","Tester","Alice.Tester@somewhere.tt",CustomerStatus.Contacted.name)
        

        val expectedNumberInOffice2 = 1

        //when
        val numberInOffice2 = create.selectCount()
                .from(CUSTOMERS)
                .where(CUSTOMERS.EMAIL.contains("somewhere"))
                .fetchOneit.value1()

        //then
        assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)

    

    @Test
    @Transactional
    open fun insertMultipleWithExtension()
        //given
        dbContainer.execute 
            insertInto(CUSTOMERS)
                    .columns(CUSTOMERS.FIRSTNAME,CUSTOMERS.LASTNAME,CUSTOMERS.EMAIL, CUSTOMERS.STATUS)
                    .values("Alice","Make","Alice.Make@somewhere.tt", CustomerStatus.Customer.name)
                    .values("Bob","Another","Bob.Another@somewhere.tt", CustomerStatus.ClosedLost.name)
                    .values("Charlie","Integration","Charlie.Integration@somewhere.tt",CustomerStatus.NotContacted.name)
                    .values("Denise","Test","Denise.Test@somewhere.tt",CustomerStatus.Customer.name)
                    .values("Ellie","Now","Ellie.Now@somewhere.tt",CustomerStatus.Contacted.name)
        

        val expectedNumberInOffice2 = 5

        //when
        val numberInOffice2 = create.selectCount()
                .from(CUSTOMERS)
                .where(CUSTOMERS.EMAIL.contains("somewhere"))
                .fetchOneit.value1()

        //then
        assertThat(numberInOffice2).isEqualTo(expectedNumberInOffice2)
    


【问题讨论】:

【参考方案1】:

Spring @Transactional 注释不仅可以神奇地与您的 DriverManager 创建的 JDBC 连接一起使用。你的 dbContainer 对象应该在你的 spring 管理的数据源上运行。

【讨论】:

嗯,这很有道理。因此,如果我为我的扩展函数创建一个辅助对象,将 Spring 维护的 DSLContext 注入其中,并使用该对象而不是从新的 jdbc 连接创建我自己的,它应该可以工作吗?谢谢,过几个小时试试。

以上是关于尽管存在`@Transactional`,为啥这些数据库修改不回滚?的主要内容,如果未能解决你的问题,请参考以下文章

为啥没有@Transactional 我可以保存? [复制]

为啥@Transactional 会自动保存到数据库

为啥将@Transactional 与@Service 一起使用而不是与@Controller 一起使用

为啥将@Transactional 与@Service 一起使用而不是与@Controller 一起使用

为啥使用 Spring Data JPA 更新实体时@Transactional 隔离级别不起作用?

Spring 声明式事务@Transactional不起作用,为啥