Search code examples
javaspring-bootspring-datamulti-tenantspring-restcontroller

REST POST service to persist into two different datasource using AbstractRoutingDataSource


Hi everyone I've follow this guide to realize a Multi-tenant application with Spring-Boot:

https://www.baeldung.com/spring-abstract-routing-data-source

Everything works fine, I use an interceptor to intercept http request and based on my business logic set tanant_A or tenant_B. In my use case I have only one scenario, where I have to set tenant_A, persist data on this DataSource in a Transaction, and after that I have to persist the same data on tenant B using the same Entities and Repositories (tenant_B is a REPLICA of tenant_A).

Example my REST controller:

    @RequestMapping(method = RequestMethod.POST,
            path = "/save",
            produces = MediaType.APPLICATION_JSON_VALUE)
    Optional<StatusMessage> create(@RequestBody MyResource resource){
        MyEntity a = mapper.resourceToEntity(resource);
        service.saveToA(a); /*Trasactional @Service use default dataSource A */
        TenantContext.clearTenantType();
        TenantContext.setCurrentTenant(DatasourceType.TENANT_B);
        service.saveToB(a); /*Trasactional @Service use dataSource B */
        return Optional.of(new StatusMessage("200","saved"));
    }

Service:

@Service
public class MyService implements IMyService {

    @Autowired
    private MyEntityRepository repository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveToA(MyEntity a) {
        repository.save(a);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveToB(MyEntity a) {
        repository.save(e);
    }

Repository:

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity ,String> {}

The first persistance transaction always works. If I change the persistance order and set tenant_B and after tenant_A, data are persisted on the first tenant I set but not in the second one.

It'seems that the second transactional method, call determineCurrentLookupKey method of my AbstractRoutingDataSource implementation, but the repository inside the service continue to use always the first tenant I set.

The strange thing is that if I repeat this steps not inside a REST Controller, but from a simple main method, the dataSource switch correctly when @Transactional method is called.

Have you got any suggestion?

Thanks.

To complete the information:

@Configuration
public class MultitenantConfiguration {

    @Value("${A.JDBC.USERNAME}")
    private String username;

    @Value("${A.JDBC.PASSWORD}")
    private String password;

    @Value("${A.JDBC.CONNECTIONURL}")
    private String url;

    @Value("${B.JDBC.USERNAME}")
    private String username_stg;

    @Value("${B.JDBC.PASSWORD}")
    private String password_stg;

    @Value("${B.JDBC.CONNECTIONURL}")
    private String url_stg;


    @Primary
    @Bean
    public DataSource dataSource() {
        MultitenantDataSourceRouter dataSource = new MultitenantDataSourceRouter();
        Map<Object, Object> resolvedDataSources = new HashMap<>();
        resolvedDataSources.put(DatasourceType.TENANT_A, dataSourceMaster());
        resolvedDataSources.put(DatasourceType.TENANT_B, dataSourceSlave());
        dataSource.setTargetDataSources(resolvedDataSources);
        dataSource.setDefaultTargetDataSource(dataSourceMaster());
        dataSource.afterPropertiesSet();
        return dataSource;
    }


    public DataSource dataSourceMaster() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        return dataSource;
    }


    public DataSource dataSourceSlave() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUsername(username_stg);
        dataSource.setPassword(password_stg);
        dataSource.setUrl(url_stg);
        return dataSource;
    }

    @Bean
    DataSourceTransactionManager transactionManager() {
        DataSourceTransactionManager txm = new DataSourceTransactionManager(dataSource());
        return txm;
    }

And this is my implementation of determineCurrentLookupKey

public class MultitenantDataSourceRouter extends AbstractRoutingDataSource {

    private static final Logger log = LoggerFactory.getLogger(MultitenantDataSourceRouter.class);

    @Override
    public Object determineCurrentLookupKey() {
        log.info(">>> determineCurrentLookupKey thread: {},{}",Thread.currentThread().getId(), Thread.currentThread().getName() );
        log.info(">>> RoutingDataSource: {}", TenantContext.getCurrentTenant());
        return TenantContext.getCurrentTenant();
    }

}

My ThreadLocal

public class TenantContext {
    private static ThreadLocal<DatasourceType> currentTenant = new ThreadLocal<>();

    public static void setCurrentTenant(DatasourceType tenant) {
        currentTenant.set(tenant);
    }

    public static DatasourceType getCurrentTenant() {
        return currentTenant.get();
    }

    public static void clearTenantType() {
        currentTenant.remove();
    }
}

If I run this simple TEST:

@RunWith(SpringRunner.class)
@SpringBootTest
public class FornitorePaylineInvokerIT{


    @Autowired
    private CreatorController creatorController;

    @Test
    public void execute() throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();

        MyResource resource = objectMapper.readValue(request.toString(), MyResource.class);

        Optional<StatusMessage> result = creatorController.create(resource);

        System.out.println(result);
    }

I see in the log 4 call to determineCurrentLookupKey:

MultitenantDataSourceRouter;28/11/2018 13:05:33,487;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:33,487;>>> RoutingDataSource: TENANT:A
MyService;28/11/2018 13:05:33,597;[BEFORE] com.services.MyService.save[MyEntity...]
MyService;28/11/2018 13:05:33,597;>>> MyService thread: 1,main
MyService;28/11/2018 13:05:33,597;MyService START on: TENANT_A
MultitenantDataSourceRouter;28/11/2018 13:05:33,644;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:33,644;>>> RoutingDataSource: PRODUZIONE
Service;28/11/2018 13:05:34,003;[AFTER] cMyService.save[MyEntity
MultitenantDataSourceRouter;28/11/2018 13:05:34,018;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:34,018;>>> RoutingDataSource: TENANT_B
MyService;28/11/2018 13:05:34,081;[BEFORE] MyService.save[MyEntity..]
MyService;28/11/2018 13:05:34,081;>>> MyService thread: 1,main
MyService;28/11/2018 13:05:34,081;MyService START on: TENANT_B
MultitenantDataSourceRouter;28/11/2018 13:05:34,081;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:34,081;>>> RoutingDataSource: TENANT_B
MyService;28/11/2018 13:05:34,288;[AFTER] com.cervedgroup.viscus.services.MyService.save[MyEntity..]
MyController;28/11/2018 13:05:34,297;[AFTER] MyController.create[MyResource...]

If I call the same service from the controller from http call

MultitenantDataSourceRouter;28/11/2018 13:05:33,487;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:33,487;>>> RoutingDataSource: TENANT:A
MyService;28/11/2018 13:05:33,597;[BEFORE] com.services.MyService.save[MyEntity...]
MyService;28/11/2018 13:05:33,597;>>> MyService thread: 1,main
MyService;28/11/2018 13:05:33,597;MyService START on: TENANT_A
MultitenantDataSourceRouter;28/11/2018 13:05:33,644;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:33,644;>>> RoutingDataSource: PRODUZIONE
Service;28/11/2018 13:05:34,003;[AFTER] cMyService.save[MyEntity
MultitenantDataSourceRouter;28/11/2018 13:05:34,018;>>> determineCurrentLookupKey thread: 1,main
MultitenantDataSourceRouter;28/11/2018 13:05:34,018;>>> RoutingDataSource: TENANT_B
MyService;28/11/2018 13:05:34,081;[BEFORE] MyService.save[MyEntity..]
MyService;28/11/2018 13:05:34,081;>>> MyService thread: 1,main
MyService;28/11/2018 13:05:34,081;MyService START on: TENANT_B
MyService;28/11/2018 13:05:34,288;[AFTER] com.cervedgroup.viscus.services.MyService.save[MyEntity..]
MyController;28/11/2018 13:05:34,297;[AFTER] MyController.create[MyResource...]

Solution

  • The problem is with my Repository Class:

    @Repository
    public interface MyEntityRepository extends JpaRepository<MyEntity ,String> {}
    

    If I use a low level implementation using JdbcTemplate this work well:

    @Repository
    public class MyEntityRepository {
    
        private final JdbcTemplate jdbcTemplate;
    
        public MyEntityRepository(DataSource datasource) {
            this.jdbcTemplate = new JdbcTemplate(datasource);
        }
    
        public void save(MyEntity e) {
            jdbcTemplate.update("INSERT INTO TABLE (PARAM_A, PARAM_B) VALUES(?,?)",
                    new Object[]{e.getParamA(), e.getParamB()});
        }
    
    }
    

    I persist the same information into 2 database MASTER and REPLICA in a single REST call.

    I don't know how forse JpaRepository to re-call getConnection when the second call is handled.