Skip to content

@Sql fails if DataSource is wrapped in a TransactionAwareDataSourceProxy #36611

@jmaniquet

Description

@jmaniquet

Hi Spring team

I would like to report a behaviour I encoutered using TransactionAwareDataSourceProxy in my tests, which I think may be a bug.

I used Spring Boot 4.0.3 but I believe any 4.0.X and earlier versions exhibit the same behaviour (see below).

I have a bunch of DB related sliced tests. A mix of @JdbcTest and @DataJdbcTest to be specific.

  • Flyway to initialize the database schema
  • Many of the tests use @Sql to prepare Data
  • I do assertions on the state of the database using the AssertJ DB library

This is not directly related to AssertJ DB, but the way I use it led me to encounter the problem.
The AssertJ DB library is (obviously) an AssertJ-like library providing assertions on the database.
It needs a DataSource as an input. And since it is not a Spring library, it is not aware of the Spring transaction.

My current use of it is to wrap the DataSource with TransactionAwareDataSourceProxy for every call to the library; and then perform the DB assertions.

It works fine, but I wanted to know if I could avoid doing the wrapping on each call, by wrapping the autoconfigured DataSource only once with the proxy at the context level.

The Javadoc for BeanPostProcessor made it look like a good fit (the postProcessAfterInitialization part) :

Typically, post-processors that populate beans via marker interfaces or the like will implement postProcessBeforeInitialization, while post-processors that wrap beans with proxies will normally implement postProcessAfterInitialization.

This looks like this :

@SpringBootApplication
public class DsTxAwareApp {

    public static void main(String[] args) {
        SpringApplication.run(DsTxAwareApp.class, args);
    }

    @Bean
    static TxAwareDataSourceBeanPostProcessor txAwareDataSourceBeanPostProcessor() {
        return new TxAwareDataSourceBeanPostProcessor();
    }

    static class TxAwareDataSourceBeanPostProcessor implements BeanPostProcessor {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof DataSource targetDataSource) {
                return new TransactionAwareDataSourceProxy(targetDataSource);
            }

            return bean;
        }
    }
}

After that, I was expecting that any interaction with the DataSource would be aware of the Spring transaction, without having to do the wrapping again.
It works in some cases, however any tests using @Sql fails with something like this :

java.lang.IllegalStateException: Failed to execute SQL scripts for test context [...] 
the configured DataSource [org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy] (named '') is not the one associated with transaction manager [org.springframework.jdbc.support.JdbcTransactionManager] (named '')
at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.executeSqlScripts(SqlScriptsTestExecutionListener.java:355)

This happens when executing the @Sql, so independently of any use of the AssertJ DB Library; hence unrelated.

Here is a simplified version of the test, using @Sql and raw Jdbc Usage (no AssertJ DB) :

@JdbcTest(includeFilters = @Filter(classes = FooJdbcDao.class, type = ASSIGNABLE_TYPE))
class FooJdbcDaoTest {
    @Autowired
    private FooJdbcDao underTest;

    @Autowired
    private DataSource dataSource;

    @Test
    @Sql(statements = "insert into FOO (ID, BAR) values (12, 'dummy')")
    void testWithExistingRow() {

        assertThat(count()).isEqualTo(1);

        Foo foo = Foo.builder()
                    .id(17)
                    .bar("whatever")
                    .build();

        underTest.insertFoo(foo);

        assertThat(count()).isEqualTo(2);
    }

    private int count() {
        try (
                Connection con = dataSource.getConnection();
                PreparedStatement statement = con.prepareStatement("select count(*) as count from foo");
                ResultSet result = statement.executeQuery()
        ) {
            result.next();
            return result.getInt("count");
        } catch (SQLException e) {
            throw new RuntimeException("Error during count");
        }
    }
}

My understanding is that it happens in the SqlScriptsTestExecutionListener, at a point where there is some logic to match a DataSource against its associated PlatformTransactionManager. Maybe it has to do with apps using several DataSource (just for my knowledge) ?

Now when trying the BeanPostProcessor approach, I saw that in the DataSourceTransactionManager there is a need to check for such a proxy.
I then assumed that there could be other places in the framework where that was necessary; but maybe my use case was never intended to be supported.

I put up a reproducer on my gitlab : https://gitlab.com/jmaniquet-prototypes/data-source-tx-aware

  • main branch : @JdbcTest / @Sql - no BeanPostProcessor, verifies data by raw Jdbc, wrapping the DataSource on each call interacting directly with
  • tx-aware-proxy-with-bean-post-processor branch : remove the wrapping on each call, and use the BeanPostProcessor - @Sql then tests starts failing
  • older-spring-boot - Same with older versions of Spring Boot - I wanted to see if at some point it used to work (it did not)

There is an additional branch with more complex set up which may be to much for a reproducer : AssertJ DB is introduced and there is additional tests for @DataJdbcTest and @MybatisTest (integration with mybatis).
I wanted to know if other sliced tests had the same behaviour (they do).

This may be a spring only problem; not boot; I opened the ticket here, my setup being boot only.

If it is not intended to be supported, maybe this should be documented somewhere ?

Let me know if you need me to prune the repo or alter it (or if maybe I am doing something wrong).

Thanks for this amazing framework

Related Issues

Metadata

Metadata

Assignees

Labels

in: testIssues in the test modulestatus: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions