声明性事务和 TransactionAwareDataSourceProxy 与 JOOQ 结合的问题

Posted

技术标签:

【中文标题】声明性事务和 TransactionAwareDataSourceProxy 与 JOOQ 结合的问题【英文标题】:Issue with Declarative Transactions and TransactionAwareDataSourceProxy in combination with JOOQ 【发布时间】:2018-08-15 08:20:07 【问题描述】:

我有一个如下所示的数据源配置类,带有单独的 DataSource bean,用于使用 JOOQ 的测试和非测试环境。在我的代码中,我不使用DSLContext.transaction(ctx -> ...,而是将该方法标记为事务性的,以便JOOQ 遵循Spring 的事务性声明性事务。我正在使用 Spring 4.3.7.RELEASE

我有以下问题:

在测试 (JUnit) 期间,@Transactional 按预期工作。无论我使用多少次DSLContextstore() 方法,单个方法都是事务性的,而RuntimeException 会触发整个事务的回滚。 在实际生产运行时,@Transactional 被完全忽略。方法不再是事务性的,TransactionSynchronizationManager.getResourceMap() 拥有两个单独的值:一个显示给我的连接池(不是事务性的),一个显示TransactionAwareDataSourceProxy)。

在这种情况下,我希望只有一个 TransactionAwareDataSourceProxy 类型的资源来包装我的 DB CP。

在使用我所做的第二组配置更改(下面用“AFTER”注明)进行多次试验和错误之后,@Transactional 即使在运行时也可以正常工作,尽管TransactionSynchronizationManager.getResourceMap() 具有以下值:

在这种情况下,我的DataSourceTransactionManager 似乎甚至不知道TransactionAwareDataSourceProxy(很可能是因为我传递了简单的DataSource,而不是代理对象),这似乎完全“跳过”了代理无论如何。

我的问题是:我的初始配置似乎是正确的,但没有奏效。建议的“修复”有效,但 IMO 根本不应该工作(因为事务管理器似乎不知道 TransactionAwareDataSourceProxy)。

这里发生了什么?有没有更简洁的方法来解决这个问题?

之前(在运行时不是事务性的)

@Configuration
@EnableTransactionManagement
@RefreshScope
@Slf4j
public class DataSourceConfig 

    @Bean
    @Primary
    public DSLContext dslContext(org.jooq.Configuration configuration) throws SQLException 
        return new DefaultDSLContext(configuration);
    

    @Bean
    @Primary
    public org.jooq.Configuration defaultConfiguration(DataSourceConnectionProvider dataSourceConnectionProvider) 
        org.jooq.Configuration configuration = new DefaultConfiguration()
            .derive(dataSourceConnectionProvider)
            .derive(SQLDialect.POSTGRES_9_5);
        configuration.set(new DeleteOrUpdateWithoutWhereListener());
        return configuration;
    

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) 
        return new DataSourceTransactionManager(dataSource);
    

    @Bean
    public DataSourceConnectionProvider dataSourceConnectionProvider(DataSource dataSource) 
        return new DataSourceConnectionProvider(dataSource);
    

    @Configuration
    @ConditionalOnClass(EmbeddedPostgres.class)
    static class EmbeddedDataSourceConfig 

        @Value("$spring.jdbc.port")
        private int dbPort;

        @Bean(destroyMethod = "close")
        public EmbeddedPostgres embeddedPostgres() throws Exception 
            EmbeddedPostgres embeddedPostgres = EmbeddedPostgresHelper.startDatabase(dbPort);
            return embeddedPostgres;
        

        @Bean
        @Primary
        public DataSource dataSource(EmbeddedPostgres embeddedPostgres) throws Exception 
            DataSource dataSource = embeddedPostgres.getPostgresDatabase();
            return new TransactionAwareDataSourceProxy(dataSource);
        
    

    @Configuration
    @ConditionalOnMissingClass("com.opentable.db.postgres.embedded.EmbeddedPostgres")
    @RefreshScope
    static class DefaultDataSourceConfig 

        @Value("$spring.jdbc.url")
        private String url;

        @Value("$spring.jdbc.username")
        private String username;

        @Value("$spring.jdbc.password")
        private String password;

        @Value("$spring.jdbc.driverClass")
        private String driverClass;

        @Value("$spring.jdbc.MaximumPoolSize")
        private Integer maxPoolSize;

        @Bean
        @Primary
        @RefreshScope
        public DataSource dataSource() 
            log.debug("Connecting to datasource: ", url);
            HikariConfig hikariConfig = buildPool();
            DataSource dataSource = new HikariDataSource(hikariConfig);
            return new TransactionAwareDataSourceProxy(dataSource);
        

        private HikariConfig buildPool() 
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(url);
            config.setUsername(username);
            config.setPassword(password);
            config.setDriverClassName(driverClass);
            config.setConnectionTestQuery("SELECT 1");
            config.setMaximumPoolSize(maxPoolSize);

            return config;
        
    

AFTER(运行时的事务性,正如预期的那样,所有未列出的 bean 与上述相同)

@Configuration
@EnableTransactionManagement
@RefreshScope
@Slf4j
public class DataSourceConfig 

    @Bean
    public DataSourceConnectionProvider dataSourceConnectionProvider(TransactionAwareDataSourceProxy dataSourceProxy) 
        return new DataSourceConnectionProvider(dataSourceProxy);
    

    @Bean
    public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy(DataSource dataSource) 
        return new TransactionAwareDataSourceProxy(dataSource);
    

    @Configuration
    @ConditionalOnMissingClass("com.opentable.db.postgres.embedded.EmbeddedPostgres")
    @RefreshScope
    static class DefaultDataSourceConfig 

        @Value("$spring.jdbc.url")
        private String url;

        @Value("$spring.jdbc.username")
        private String username;

        @Value("$spring.jdbc.password")
        private String password;

        @Value("$spring.jdbc.driverClass")
        private String driverClass;

        @Value("$spring.jdbc.MaximumPoolSize")
        private Integer maxPoolSize;

        @Bean
        @Primary
        @RefreshScope
        public DataSource dataSource() 
            log.debug("Connecting to datasource: ", url);
            HikariConfig hikariConfig = buildPoolConfig();
            DataSource dataSource = new HikariDataSource(hikariConfig);
            return dataSource; // not returning the proxy here
        
    

【问题讨论】:

作为记录,这个问题似乎也被问到了 spring boot 问题跟踪器:github.com/spring-projects/spring-boot/issues/12377 嗨@LukasEder,是的,我已经单独发布了这个问题,尽管它似乎放错了地方。我把它移到了SO。对于没有在此处提及上述内容而造成的混乱,我深表歉意。感谢您更新此问题和跟踪器。 您的课程是否直接使用DataSource(例如JDBC)?如果没有,我认为您根本不应该使用TransactionAwareDataSourceProxy。正如the docs 中所述,“请注意,事务管理器,例如 DataSourceTransactionManager,仍然需要使用底层 DataSource,而不是使用此代理。” 如果您的代码确实使用了 JDBC 以及 Spring 的抽象,那么您将需要创建两个 bean。首先是普通数据源,您将其传递给 Spring 的抽象(例如 DataSourceTransactionManager)和第二个 bean,即代理,它包装第一个 bean 并将其传递给您的类。 【参考方案1】:

我会把我的 cmets 变成答案。

事务管理器不应该知道代理。来自documentation:

注意事务管理器,例如 DataSourceTransactionManager,仍然需要与底层合作 数据源,不使用此代理。

TransactionAwareDataSourceProxy 类是一个特殊用途的类,在大多数情况下不需要。任何通过 Spring 框架基础设施与您的数据源交互的东西都不应在其访问链中包含代理。代理适用于无法与 Spring 基础架构交互的代码。例如,已经设置为使用 JDBC 并且不接受任何 Spring 的 JDBC 模板的第三方库。这在与上述相同的文档中有所说明:

此代理允许数据访问代码使用纯 JDBC API 和 仍然参与 Spring 管理的事务,类似于 JDBC 代码 在 J2EE/JTA 环境中。但是,如果可能,请使用 Spring 的 DataSourceUtils、JdbcTemplate 或 JDBC 操作对象获取 即使没有目标的代理也参与交易 DataSource,避免一开始就定义这样的代理 地点。

如果您没有任何需要绕过 Spring 框架的代码,则根本不要使用 TransactionAwareDataSourceProxy。如果您确实有这样的遗留代码,那么您将需要执行您在第二次设置中已经配置的内容。您将需要创建两个 bean,一个是数据源,一个是代理。然后,您应该将数据源提供给所有 Spring 托管类型,并将代理提供给旧类型。

【讨论】:

啊,现在一切都说得通了。在这种情况下,JOOQ 在 Spring 眼中是“遗留代码”,因此它的 DB 访问必须通过连接提供程序进行,该连接提供程序从 TransactionAwareDataSourceProxy 检索连接。无论何时使用DSLContext,第二个配置都会隐式授予对代理对象的访问权限。非常感谢您的宝贵时间。

以上是关于声明性事务和 TransactionAwareDataSourceProxy 与 JOOQ 结合的问题的主要内容,如果未能解决你的问题,请参考以下文章

事务并发传播性隔离级别

Spring 声明式事务

spring学习笔记声明式事务

Spring_事务

Spring-声明式事物

长文捋明白 Spring 事务!隔离性?传播性?一网打尽!