@事务在多数据源Spring Boot+;MyBatis应用

@事务在多数据源Spring Boot+;MyBatis应用,spring,spring-boot,transactions,mybatis,spring-mybatis,Spring,Spring Boot,Transactions,Mybatis,Spring Mybatis,我正在尝试配置SpringBoot+MyBatis应用程序,它应该可以与多个数据源一起工作。我试着做类似的事情 查询和更新数据是有效的,但是当我编写单元测试时,我发现@Transactional不起作用。事务必须在所有数据库之间工作。这意味着,如果一个带有@Transactional的方法在两个数据库上都进行了更新,那么在出现异常时,所有内容都应该回滚 这是一个用于测试目的和同事的示例应用程序。成功配置后,将以类似方式配置和开发新应用程序 马文: <?xml version="1.0" e

我正在尝试配置SpringBoot+MyBatis应用程序,它应该可以与多个数据源一起工作。我试着做类似的事情

查询和更新数据是有效的,但是当我编写单元测试时,我发现@Transactional不起作用。事务必须在所有数据库之间工作。这意味着,如果一个带有@Transactional的方法在两个数据库上都进行了更新,那么在出现异常时,所有内容都应该回滚

这是一个用于测试目的和同事的示例应用程序。成功配置后,将以类似方式配置和开发新应用程序

马文:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.aze.mybatis</groupId>
    <artifactId>sample-one</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.4.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
配置到数据库BSC(Oracle)

配置到数据库ONSUBS(Oracle)

BSC注释:

package com.aze.mybatis.sampleone.config;

public @interface BscsDataSource {
}
和ONSUBS:

package com.aze.mybatis.sampleone.config;

public @interface OnsubsDataSource {
}
应与ONSUBS配合使用的映射器接口:

package com.aze.mybatis.sampleone.dao;

import com.aze.mybatis.sampleone.config.OnsubsDataSource;
import com.aze.mybatis.sampleone.domain.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
@OnsubsDataSource
public interface PaymentDao {

    Payment getPaymentById(@Param("paymentId") Integer paymentId);

}
和理学士:

package com.aze.mybatis.sampleone.dao;

import com.aze.mybatis.sampleone.config.BscsDataSource;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
@BscsDataSource
public interface PostpaidCustomerDao {

    PostpaidBalance getPostpaidBalance(@Param("customerId") Integer customerId);

    // BigDecimal amount may be used as second parameter, but I want to show, how to work with two parameters where second is object
    void updateDepositAmount(@Param("customerId") Integer customerId, @Param("balance") PostpaidBalance postpaidBalance);

    void updateAzFdlLastModUser(@Param("customerId") Integer customerId, @Param("username") String username);

}
下面是带有@Transactional的代码

package com.aze.mybatis.sampleone.service;

import com.aze.mybatis.sampleone.dao.PaymentDao;
import com.aze.mybatis.sampleone.dao.PostpaidCustomerDao;
import com.aze.mybatis.sampleone.domain.Payment;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import com.aze.mybatis.sampleone.exception.DataNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class PaymentServiceImpl implements PaymentService {

    private static final String MIN_DEPOSIT_AMOUNT = "150";

    @Autowired
    private PaymentDao paymentDao;

    @Autowired
    private PostpaidCustomerDao postpaidCustomerDao;

    @Override
    public PostpaidBalance getPostpaidBalance(Integer customerId) {
        PostpaidBalance balance = postpaidCustomerDao.getPostpaidBalance(customerId);

        if (balance == null) {
            throw new DataNotFoundException(String.format("Can't find any balance information for customer with customer_id = %d", customerId));
        }

        return balance;
    }

    // Note. By default rolling back on RuntimeException and Error but not on checked exceptions
    // If you want to rollback on check exception too then add "rollbackFor = Exception.class"
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateDepositAmount(Integer customerId, PostpaidBalance postpaidBalance, String username) {
        postpaidCustomerDao.updateDepositAmount(customerId, postpaidBalance);

        // In case of @Transactional annotation, you can use method from the same class if it doesn't change data on database
        PostpaidBalance balance = getPostpaidBalance(customerId);

        // This logic is for showing that how the @Transactional annotation works.
        // Because of the exception, the previous transaction will rollback
        if (balance.getDeposit().compareTo(new BigDecimal(MIN_DEPOSIT_AMOUNT)) == -1) {
            throw new IllegalArgumentException("The customer can not have deposit less than " + MIN_DEPOSIT_AMOUNT);
        }

        // In case of @Transactional annotation, you must not (!!!) use method from the same (!) class if it changes data on database
        // That is why, postpaidCustomerDao.updateAzFdlLastModUser() used here instead of this.updateAzFdlLastModUser()
        postpaidCustomerDao.updateAzFdlLastModUser(customerId, username);

        // If there is no exception, the transaction will commit
    }
}
以下是单元测试代码:

package com.aze.mybatis.sampleone.service;

import com.aze.mybatis.sampleone.Application;
import com.aze.mybatis.sampleone.config.BscsDatabaseConfig;
import com.aze.mybatis.sampleone.config.OnsubsDatabaseConfig;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.math.BigDecimal;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {Application.class, OnsubsDatabaseConfig.class, BscsDatabaseConfig.class})
@TestPropertySource(locations= "classpath:application.properties")
public class PaymentServiceImplTest extends Assert {


    // My goal is not to write a full and right unit tests, but just show you examples of working with MyBatis

    @Autowired
    private PaymentService paymentService;

    @Before
    public void setUp() throws Exception {
        assert paymentService != null;
    }

    @Test
    public void updateDepositAmount() throws Exception {
        final int customerId = 4301887; // not recommended way. Just for sample
        final String username = "ITCSC";
        boolean exceptionRaised = false;

        PostpaidBalance balance = paymentService.getPostpaidBalance(customerId);
        assertTrue("Find customer with deposit = 0", balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);
        balance.setDeposit(BigDecimal.TEN);

        try {
            paymentService.updateDepositAmount(customerId, balance, username);
        } catch (Exception e) {
            exceptionRaised = true;
        }

        assertTrue(exceptionRaised);
        balance = paymentService.getPostpaidBalance(customerId);
        // We check that transaction was rollback and amount was not changed
        assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);

        final BigDecimal minDepositAmount = new BigDecimal("150");
        balance.setDeposit(minDepositAmount);
        paymentService.updateDepositAmount(customerId, balance, username);

        balance = paymentService.getPostpaidBalance(customerId);
        assertTrue(balance.getDeposit().compareTo(minDepositAmount) != -1);
    }
}
assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO)==0)上的单元测试失败。我检查了数据库,看到第一次更新
postaidcustomerdao.updatedepositionamount(customerId,postaidbalance)未回滚


请帮助解决此问题。

如果您有多个TransactionManager,则需要使用Bean名称或限定符引用要用于
@Transactional
的Transactional Manager

在Java配置中:

@Bean("myTM")
public PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(myDatasource());
}
在职:

@Transactional("myTM")
public void insertWithException(JdbcTemplate jdbcTemplate) {
}
我调试了我的应用程序,看看Spring如何选择TransactionManager。这是在
org.springframework.transaction.interceptor.TransactionSpectSupport#determinetTransactionManager

/**
 * Determine the specific transaction manager to use for the given transaction.
 */
protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
    // Do not attempt to lookup tx manager if no tx attributes are set
    if (txAttr == null || this.beanFactory == null) {
        return getTransactionManager();
    }
    String qualifier = txAttr.getQualifier();
    if (StringUtils.hasText(qualifier)) {
        return determineQualifiedTransactionManager(qualifier);
    }
    else if (StringUtils.hasText(this.transactionManagerBeanName)) {
        return determineQualifiedTransactionManager(this.transactionManagerBeanName);
    }
    else {
        PlatformTransactionManager defaultTransactionManager = getTransactionManager();
        if (defaultTransactionManager == null) {
            defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
            if (defaultTransactionManager == null) {
                defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
                this.transactionManagerCache.putIfAbsent(
                        DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
            }
        }
        return defaultTransactionManager;
    }
}

因此,它只是获取默认的主
PlatformTransactionManager
bean。

如果您有多个TransactionManager,则需要使用bean名称或限定符引用要用于
@Transactional
的TransactionManager

在Java配置中:

@Bean("myTM")
public PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(myDatasource());
}
在职:

@Transactional("myTM")
public void insertWithException(JdbcTemplate jdbcTemplate) {
}
我调试了我的应用程序,看看Spring如何选择TransactionManager。这是在
org.springframework.transaction.interceptor.TransactionSpectSupport#determinetTransactionManager

/**
 * Determine the specific transaction manager to use for the given transaction.
 */
protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
    // Do not attempt to lookup tx manager if no tx attributes are set
    if (txAttr == null || this.beanFactory == null) {
        return getTransactionManager();
    }
    String qualifier = txAttr.getQualifier();
    if (StringUtils.hasText(qualifier)) {
        return determineQualifiedTransactionManager(qualifier);
    }
    else if (StringUtils.hasText(this.transactionManagerBeanName)) {
        return determineQualifiedTransactionManager(this.transactionManagerBeanName);
    }
    else {
        PlatformTransactionManager defaultTransactionManager = getTransactionManager();
        if (defaultTransactionManager == null) {
            defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
            if (defaultTransactionManager == null) {
                defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
                this.transactionManagerCache.putIfAbsent(
                        DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
            }
        }
        return defaultTransactionManager;
    }
}

因此,它只是获取默认的主
PlatformTransactionManager
bean。

您看过JtaTransactionManager的分布式事务吗?没有,没有看过JtaTransactionManager。正如您在这里看到的,两个更新操作都在一个DAO(在一个数据库中)postpaidCustomerDao中。事务管理即使在一个数据库上也不起作用。明天我将尝试JtaTransactionManager。啊,是的,我的坏想法是它击中了两个数据源。也许需要考虑的另一件事是,您使用的是事务性事务而不指定特定的事务管理器,而您的主数据源/事务管理器是在SUBS上的,而您的测试正在使用BSCSDATABOCECES来攻击PaPaDeCuffelDAO,但是“Exchange”如何知道您声明的两个事务管理器中的哪一个?如果您没有指定,在您的实例中使用?我的理解是,这将默认为Primary,我相信我能够在我拥有的示例项目中复制这一点,并证实我的怀疑。使用不与我正在更新的数据源绑定的主事务管理器的事务管理器时,尽管会引发错误,但仍会提交。但是,如果我更新事务以指定适当的事务管理器,则会按预期回滚更新。是否查看了用于分布式事务的JtaTransactionManager?否,没有看过JtaTransactionManager。正如您在这里看到的,两个更新操作都在一个DAO(在一个数据库中)postpaidCustomerDao中。事务管理即使在一个数据库上也不起作用。明天我将尝试JtaTransactionManager。啊,是的,我的坏想法是它击中了两个数据源。也许需要考虑的另一件事是,您使用的是事务性事务而不指定特定的事务管理器,而您的主数据源/事务管理器是在SUBS上的,而您的测试正在使用BSCSDATABOCECES来攻击PaPaDeCuffelDAO,但是“Exchange”如何知道您声明的两个事务管理器中的哪一个?如果您没有指定,在您的实例中使用?我的理解是,这将默认为Primary,我相信我能够在我拥有的示例项目中复制这一点,并证实我的怀疑。使用不绑定到我正在更新的数据源的主事务管理器的事务管理器进行提交,尽管引发了错误,但如果我更新事务以指定适当的事务管理器,则会按预期回滚更新