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。
大家好,我已经按照本指南使用 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。