8

I use AbstractRoutingDataSource to change data source dynamically and ThreadLocal to set up currentLookupKey. It works nice when I use only one data source per http request. I use JpaRepository

@Component
@Primary
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Autowired
    private DatabaseMap databaseMap;

    @Override
    public void afterPropertiesSet() {
        setTargetDataSources(databaseMap.getSourcesMap());
        setDefaultTargetDataSource(databaseMap.getSourcesMap().get("DEFAULT"));
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseType();
    }

}

public class DatabaseContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public static void setDatabaseType(String string) {
        contextHolder.set(string);
    }

    public static String getDatabaseType() {
        return (String) contextHolder.get();
    }

    public static void clearDatabaseType() {
        contextHolder.remove();
    }
}

When I try to get data in my REST controller I get data only from one database.

Some code in my REST controller

DatabaseContextHolder.setDatabaseType("db1");
//here I get data from db1 as expected
//I use JpaRepository
DatabaseContextHolder.clearDatabaseType();
DatabaseContextHolder.setDatabaseType("db2");
//here I should get data from db2 but get from db1

I tried to debug and it looks like Spring obtains data source only once in http request.

This method is called only once.

@Override
public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
}

Is there any way to force Spring to change data source.

mariusz2108
  • 851
  • 2
  • 11
  • 36

5 Answers5

2

Your problem could be related with transaction delimitation.

When you define a @Transactional annotation in your code, Spring will create on your behalf all the stuff necessary to begin and end, and commiting or rollback if required, a transaction.

As you can see in the doBegin method in the source code of the DataSourceTransactionManager class - the same applies to other transaction managers - , Spring obtains a Connection when the transaction is initialized - this is why the method getConnection is invoked only once - , and it will reuse that connection across all the underlying operations against the database within that transaction (it makes sense for ACID preservation).

So, if you need to connect to several data sources within the same request processing, you can define different methods in you service code, every one annotated with a @Transactional annotation, and change the underlying data source as you require before invoke them:

DatabaseContextHolder.setDatabaseType("db1");
// Invoke a service method annotated with @Transactional
// It can use the underlying JpaRepositories that you need
DatabaseContextHolder.clearDatabaseType();
DatabaseContextHolder.setDatabaseType("db2");
// Invoke again another (or the same, what you need) service method
// annotated with @Transactional, You should get data from db2 this time
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • The problem is that even after having completing the first transaction, hikari pool doesn't releases the connection and uses the same for the other new transaction as well. – Ankit Singodia Aug 05 '20 at 19:34
  • I understand. Can you please include information in your question about how you are configuring the actual datasources with Hikari? – jccampanero Aug 05 '20 at 19:41
  • Please check it here: https://docs.google.com/document/d/1fmdbk7wADVhVsXpDEMq-Gcy5BWhSsNI4nesUWJyHNz0/edit?usp=sharing – Ankit Singodia Aug 05 '20 at 19:50
  • @AnkitSingodia Unless it is required for any other reason, please, can you modify your code in ```DataSourceConfig``` and remove all the ```Bean```s annotations related to the different datasources (```masterDataSource```, ```slaveDataSource```, and ```routingDataSource```) and the Hikari configurations? When you initialize the entity manager factory you are actually creating all the required stuff when you set its datasource: ```em.setDataSource(routingDataSource())```. There is no need to declare also these objects as beans, it will be duplicated in the Spring context. Can you try and see? – jccampanero Aug 06 '20 at 13:01
  • Please, can you also complete the google docs document with the source code of your controller and service classes? I think it would be of help. – jccampanero Aug 06 '20 at 13:11
  • @AnkitSingodia I think the problem must be related with your configuration or with Hikari. I downloaded a similar example from this [link](https://www.adictosaltrabajo.com/2015/11/27/tutorial-de-spring-dynamic-datasource/). In the example they have two datasources ruled by ```AbstractRoutingDataSource```. Although they use XML configuration instead of Java configuration, all the software is very similar to the one you describe. In the method ```getTeams``` of the ```TeamController``` class that they define, I just did a simple test (please, see next comment), and it works as expected. – jccampanero Aug 07 '20 at 13:11
  • ```@RequestMapping(method = RequestMethod.GET) public List getTeams(@RequestParam(required = false, value = COUNTRY) String code) { CountryDbContextHolder.setDbType(mapLeagues.get("ES")); List teamsSpain = teamService.getTeams(); CountryDbContextHolder.clearDb(); CountryDbContextHolder.setDbType(mapLeagues.get("DE")); List teamsDeusch = teamService.getTeams(); teamsSpain.addAll(teamsDeusch); return teamsSpain; } ``` – jccampanero Aug 07 '20 at 13:11
0
  • My suspicion here is you have a method annotated with @Transactional annotation. Before calling that transactional method, you first specify one datasource key and you call the transactional method.Inside the transactional method, you first call repository and it works as expected with datasource look up key you set. However then you set different key, inside the transactional method, and call another repository and it still uses the key you set first time.

  • DataSource will be chosen by the framework when the transaction starts so if you are using @Transactional annotation, whatever switching you do inside the method is useless. Because the datasource would have been chosen by proxy created for @Transactional annotation. Best option is to have the branching logic in non transactional service or use TransactionTemplate instead of @Transactional

  • For example, make sure YourRestController does not have class level @Transactional as well as no @Transactional annotation in this yourRestControllerMethod, you will keep them to your service.

     @RestController
     public class YourRestController {

      @Autowired
      TransactionalService transactional

      public void yourRestControllerMethod(){
        //set the datasource look up key to A1 and then
        transactional.methodA1();
        //change datasource look up key to A2 and then
        transactional.methodA2();
      }

     }
    @Service
    public class TransactionalService {

       @Transactional
       public void methodA1(){

       }
       
       @Transactional
       public void methodA2() {

       }

    }
  • 2
    We have exactly a same setup. I debugged the problem and it boils down to the fact that: When the first transaction begins --> the datasource key is resolved -> hikari pool try creating a new connection within which it used a map of predefined (in config) datasources map and fetches the correct datasource. But then when the new transaction begins --> the datasource key is again resolved (as expected) --> BUT this time, the hikari pool doesn't tries fetching a new connection and rather uses the older one and hence it never uses the datasource map this time. – Ankit Singodia Aug 05 '20 at 19:30
  • Nope, I'm still searching for a fix :( – Ankit Singodia Aug 05 '20 at 19:30
  • Can you add your HikariDataSource configuration and any related properties both datasources? – Kavithakaran Kanapathippillai Aug 05 '20 at 19:40
  • 1
    Please check it here: https://docs.google.com/document/d/1fmdbk7wADVhVsXpDEMq-Gcy5BWhSsNI4nesUWJyHNz0/edit?usp=sharing – Ankit Singodia Aug 05 '20 at 19:49
  • What is in `getHibernatePropertiesMaster()`? – Kavithakaran Kanapathippillai Aug 05 '20 at 20:03
  • There are two datasources and with their own connection pool. So as long as when the second transaction begins, it goes to routing datasource, it does the looking, then routing datasource should pickup the `slaveHikari` datasource. slaveHikari datasource is free to reuse its connection from its own pool because it is pointing slave database anyway. – Kavithakaran Kanapathippillai Aug 05 '20 at 20:08
  • So when you are debugging can you take note of the object reference for `masterDatasource` and `slaveDataSource`, and see the datasource reference picked up by routing datasource is different from the first one? – Kavithakaran Kanapathippillai Aug 05 '20 at 20:10
  • Can you please do one more thing for me to rule out you are not mixing up properties? If you are doing master first and slave second, can you switch it to slave first and master second. As per your bug, in first scenario, both calls should go to master only and in in first scenario, both calls should go to slave db only. If both scenarios go to master, you are mixing up the properties – Kavithakaran Kanapathippillai Aug 05 '20 at 20:18
  • I just added the getHibernatePropertiesMaster() to the same google doc. For your recent comment, when I try with master first and slave second, then both the transactions works using master datasource . And when I do slave txn first and master txn second, then it uses slave datasource for both the txns (though it fails on second one as I'm trying to do a write operation there) – Ankit Singodia Aug 05 '20 at 20:50
  • I've also added a code snippet from one of the hikari jar class file, which tries fetching a connection (but sadly only at the time of first transaction in an http request) – Ankit Singodia Aug 05 '20 at 21:03
  • Unfortunately no lock. In What class has `Connection newConnection()` method. I can't see it in `HikariDataSource` class? – Kavithakaran Kanapathippillai Aug 05 '20 at 21:32
  • It's in PoolBase.java in com.zaxxer.hikari.pool package – Ankit Singodia Aug 06 '20 at 05:30
  • Facing similar kind of issue. https://stackoverflow.com/questions/76136715/transactionsynchronizationmanager-iscurrenttransactionreadonly-always-returnin – Harsh Kanakhara Apr 30 '23 at 09:29
0

I had the same issue, none of the above solution could fix it.. but making my Service method final (in my REST Controller)

public final Response
DamienG
  • 11
  • 1
  • 6
0

Set spring.jpa.open-in-view to false.

Vadik Sirekanyan
  • 3,332
  • 1
  • 22
  • 29
0

In my case, I followed the suggestion from jccampanero and it work: separating two methods in the service class, each method has a database connection with a different data source, switching datasource by AbstractRoutingDataSource.

I think the key point here is the database configuration where we give the AbstractRoutingDataSource to EntityManagerFactory and TransactionManager instead of DataSource as normal.

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return ReadOnlyContext.isReadOnly() ? DSType.READ_ONLY : DSType.WRITE;
    }
}

and the DatabaseConfiguration:

@Bean
public RoutingDataSource actualDataSource(
    @Qualifier("dataSource") DataSource readWriteDataSource,
    @Qualifier("dataSourceReadOnly") DataSource readOnlyDataSource
) {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DSType.READ_ONLY, readOnlyDataSource);
    targetDataSources.put(DSType.WRITE, readWriteDataSource);

    RoutingDataSource routingDataSource = new RoutingDataSource();
    routingDataSource.setTargetDataSources(targetDataSources);
    routingDataSource.setDefaultTargetDataSource(readOnlyDataSource);

    return routingDataSource;
}

@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
    EntityManagerFactoryBuilder builder,
    RoutingDataSource actualDataSource
) {
    return builder.dataSource(actualDataSource).packages("the.domain.package").persistenceUnit("persistenUnitName").build();
}

@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(RoutingDataSource actualDataSource) {
    return new DataSourceTransactionManager(actualDataSource);
}

With the above configuration, JPA Repositories will use the entityManagerFactory bean to obtain DB connections (in each service method, Spring will call the method determineCurrentLookupKey in RoutingDataSource to get the datasource we specified before).

ChenHuang
  • 382
  • 2
  • 11