REST POST 服务使用 AbstractRoutingDataSource 持久化到两个不同的数据源

REST POST service to persist into two different datasource using AbstractRoutingDataSource

大家好,我已经按照本指南使用 Spring-Boot 实现多租户应用程序:

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

一切正常,我使用拦截器拦截http请求并根据我的业务逻辑设置tanant_A或tenant_B。 在我的用例中,我只有一个场景,我必须设置 tenant_A,在事务中将数据保存在这个数据源上,然后我必须使用相同的实体和存储库在租户 B 上保存相同的数据( tenant_B 是 tenant_A) 的副本。

例如我的 REST 控制器:

    @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
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
public interface MyEntityRepository extends JpaRepository<MyEntity ,String> {}

第一个持久性事务始终有效。如果我更改持久性顺序并设置 tenant_B 并在 tenant_A 之后,数据将保留在我设置的第一个租户中,但不会保留在第二个租户中。

似乎是第二个事务方法,调用我的 AbstractRoutingDataSource 实现的 determineCurrentLookupKey 方法,但服务内的存储库继续使用我设置的第一个租户。

奇怪的是,如果我不是在 REST 控制器内重复此步骤,而是从一个简单的 main 方法重复此步骤,则在调用 @Transactional 方法时数据源会正确切换。

你有什么建议吗?

谢谢。

信息补全:

@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;
    }

这是我对 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();
    }

}

我的线程本地

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();
    }
}

如果我 运行 这个简单的测试:

@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);
    }

我在日志 4 中看到调用 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...]

如果我通过 http 调用从控制器调用相同的服务

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...]

问题出在我的存储库 Class:

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

如果我使用使用 JdbcTemplate 的低级实现,则效果很好:

@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()});
    }

}

我在单个 REST 调用中将相同的信息保存到 2 个数据库 MASTER 和 REPLICA 中。

我不知道 JpaRepository 如何在处理第二次调用时重新调用 getConnection。