39

I have to update two data sources as part of one transaction. That is -

  1. I do an update in DB1.
  2. Then, I do another update in DB2.

If update in DB2 fails, I want to roll back both DB1 and DB2 to roll back. Can this be accomplished using @Transactional ?

Here is a sample code -

@Transactional(value="db01TransactionManager")
public void updateDb01() {
    Entity01 entity01 = repository01.findOne(1234);
    entity01.setName("Name");
    repository01.save(entity01);

    //Calling method to update DB02
    updateDb02();
}

@Transactional(value="db02TransactionManager")
public void updateDb02() {
    Entity02 entity02 = repository02.findOne(1234);
    entity02.setName("Name");
    repository02.save(entity02);

    //Added this to force a roll back for testing
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

My problem is that, the setRollbackOnly() in updateDb02 rolls back only the the Db01 transaction.

Do Will
  • 711
  • 1
  • 9
  • 18

4 Answers4

53

I've solved this problem using ChainedTransactionManager - http://docs.spring.io/spring-data/commons/docs/1.6.2.RELEASE/api/org/springframework/data/transaction/ChainedTransactionManager.html

Spring Boot Configuration:

    @Bean(name = "chainedTransactionManager")
    public ChainedTransactionManager transactionManager(@Qualifier("primaryDs") PlatformTransactionManager ds1,
                                                    @Qualifier("secondaryDs") PlatformTransactionManager ds2) {
         return new ChainedTransactionManager(ds1, ds2);
    }

And then you can use it as follows:

@Transactional(value="chainedTransactionManager")
public void updateDb01() {
    Entity01 entity01 = repository01.findOne(1234);
    entity01.setName("Name");
    repository01.save(entity01);

    //Calling method to update DB02
    updateDb02();
}

public void updateDb02() {
    Entity02 entity02 = repository02.findOne(1234);
    entity02.setName("Name");
    repository02.save(entity02);

    //Added this to force a roll back for testing
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
kolmant
  • 649
  • 6
  • 10
  • 1
    nothing but ChainedTransactionManager worked for me. I had to deal with 4 databases and this is the only thing that could help me – Logic Apr 15 '19 at 20:18
  • 5
    For my future self and others: if you want `@Transactional` to use `ChainedTransactionManager` by default, add `@Primary` on the `ChainedTransactionManager` bean. – user2652379 Dec 13 '19 at 03:03
  • exactly my problem, exactly the right solution to apply... THANKS! – TooLiPHoNe.NeT Jan 14 '20 at 16:56
  • 2
    Here's a post I wrote illustrating how to set up a Chained Transaction Manager with some more elaborate code samples. This might help explain how all the components work together to produce this here result: https://metamorphant.de/blog/posts/2021-03-21-distributed-transactions-across-multiple-dbs-chainedtransactionmanager/ – dribnif Mar 31 '21 at 19:31
  • 9
    Please note that [ChainedTransactionManager](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/transaction/ChainedTransactionManager.html) has been **deprecated** due to its unexpected behavior. please refer to this [ticket](https://github.com/spring-projects/spring-data-commons/issues/2232) for further information. – Mo'ath Alshorman Nov 04 '21 at 10:14
3

The best way is to create a third method, that will be annotated as @Transactional.

@Transactional(readOnly = false)
public void updateCommon(){
  upbateDb01();
  upbateDb02();
}

According to a spring documentation, transaction control starts when the firts annotation appears,so in this case a single transaction will start when updateCommon will be invoked. UPDATE But this will work if you use CrudRepository or something like that.

In case of multiple datasources you may try to use a Global transaction management conception. Here is a sample from a spring documentation:

@Inject private PlatformTransactionManager txManager; 

TransactionTemplate template  = new TransactionTemplate(this.txManager); 
template.execute( new TransactionCallback<Object>(){ 
  public void doInTransaction(TransactionStatus status){ 
   // work done here will be wrapped by a transaction and committed. 
   // the transaction will be rolled back if 
   // status.setRollbackOnly(true) is called or an exception is thrown 
  } 
});

And here is a link: http://spring.io/blog/2011/08/15/configuring-spring-and-jta-without-full-java-ee/ I have never use it for my own, so I didn't explore this topic deeply. Hope it will help

Yuriy Tsarkov
  • 2,461
  • 2
  • 14
  • 28
  • 1
    My issue is that because I have multiple data sources, it will start one transactionManager with this @Transactional and only that can take part in the transaction. So, with your solution, I will still run into the issue where only one transactionManager will be applicable to all my database updates – Do Will Feb 23 '18 at 21:50
  • Damn, you're right, somehow I missed it( I will update my answer according to the write conditions – Yuriy Tsarkov Feb 24 '18 at 05:59
0

Figured it out.

The methods have to be in different beans to be able to use different transaction managers.

Do Will
  • 711
  • 1
  • 9
  • 18
0

I believe you have defined your txns like below.

@Bean(name="db01TransactionManager") 
@Autowired
DataSourceTransactionManager tm1(@Qualifier ("datasource1") DataSource datasource) {
    DataSourceTransactionManager txm  = new DataSourceTransactionManager(datasource);
    return txm;
}

@Bean(name="db02TransactionManager") 
@Autowired
DataSourceTransactionManager tm2(@Qualifier ("datasource2") DataSource datasource) {
    DataSourceTransactionManager txm  = new DataSourceTransactionManager(datasource);
    return txm;
}

Now simplest way is to try , catch and rollback for both of the transaction. But if you still want to delegate, there is an option as given below.

Create your own and override rollback methods and use it.

@Bean(name=“allTransactionManager") 
@Autowired
DataSourceTransactionManager tm2(@Qualifier ("datasource1”) DataSource datasource1, @Qualifier ("datasource2") DataSource datasource2) {

    DataSourceTransactionManager txm  = new MyDataSourceTransactionManager(datasource1,datasouce2);
        return txm;
}

And define your own transactionmanager as.

MyDataSourceTransactionManager extends DataSourceTransactionManager{
DataSourceTransactionManager tm1; 
DataSourceTransactionManager tm2; 

MyDataSourceTransactionManager(Datasource ds1,Datasource d2){
  tm1 = new DataSourceTransactionManager(DataSource);
  tm2 =new DataSourceTransactionManager(DataSource);
}
// override and for roll back, rollback for both of tm1 and tm2. Thus all actions are delegated in this class

}

Then use this for dao layers whereever you want to work synchronously.

 @Transactional("allTransactionManager")

So now we have your own transaction managers, which is capable of rollbacking or commiting together for both type of transactions.

surya
  • 2,581
  • 3
  • 22
  • 34