source 는 Github 에 있습니다.
@Configuration
@RequiredArgsConstructor
public class DataSourceConfiguration {
private static final String MAIN_PROPERTIES = "spring.datasource.main.hikari";
private static final String SUB_PROPERTIES = "spring.datasource.sub.hikari";
public static final String MAIN_DATASOURCE = "mainDataSource";
public static final String SUB_DATASOURCE = "subDataSource";
@Bean
@ConfigurationProperties(prefix = MAIN_PROPERTIES)
public HikariConfig mainHikariConfig() {
return new HikariConfig();
}
@Primary // batch job repository datasource 는 primary 설정 datasource 로 설정됨.
@Bean(MAIN_DATASOURCE)
public DataSource mainDataSource() {
return new LazyConnectionDataSourceProxy(new HikariDataSource(mainHikariConfig()));
}
@Bean
@ConfigurationProperties(prefix = SUB_PROPERTIES)
public HikariConfig subHikariConfig() {
return new HikariConfig();
}
@Bean(SUB_DATASOURCE)
public DataSource subDataSource() {
return new LazyConnectionDataSourceProxy(new HikariDataSource(subHikariConfig()));
}
}
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties({JpaProperties.class, HibernateProperties.class})
public class JpaConfiguration {
@Primary
@Bean(name = MAIN_TRANSACTION_MANAGER)
public PlatformTransactionManager mainTransactionManager(
@Qualifier(MAIN_ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
}
@Bean(name = SUB_TRANSACTION_MANAGER)
public PlatformTransactionManager subTransactionManager(
@Qualifier(SUB_ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
}
@Bean(name = CHAINED_TRANSACTION_MANAGER)
public PlatformTransactionManager chainedTransactionManager(
@Qualifier(MAIN_TRANSACTION_MANAGER) PlatformTransactionManager mainTransactionManager
,@Qualifier(SUB_TRANSACTION_MANAGER) PlatformTransactionManager subTransactionManager) {
return new ChainedTransactionManager(mainTransactionManager, subTransactionManager);
}
public static final String MAIN_REPOSITORY_PACKAGE = "com.example.batch.repository.main";
public static final String SUB_REPOSITORY_PACKAGE = "com.example.batch.repository.sub";
@Configuration
@EnableJpaRepositories(
basePackages = MAIN_REPOSITORY_PACKAGE
,entityManagerFactoryRef = JpaConfiguration.MAIN_ENTITY_MANAGER_FACTORY
,transactionManagerRef = JpaConfiguration.MAIN_TRANSACTION_MANAGER
)
public static class MainJpaRepositoriesConfig{}
@Configuration
@EnableJpaRepositories(
basePackages = SUB_REPOSITORY_PACKAGE
,entityManagerFactoryRef = JpaConfiguration.SUB_ENTITY_MANAGER_FACTORY
,transactionManagerRef = JpaConfiguration.SUB_TRANSACTION_MANAGER
)
public static class SubJpaRepositoriesConfig{}
}
아래 그림과 같이 Transaction 1, Transaction 2 가 트랜잭션을 시작하고 비즈니스 로직을 처리합니다.
비즈니스 로직을 처리한 후, 순차적으로 트랜잭션을 commit/rollback 처리하는 것이 ChainedTransactionManager 의 동작 방식입니다.
이 트랜잭션 매니저의 문제점은 Transaction 2 가 commit 된 후, Transaction 1 이 commit 에 실패할 수 있습니다.
그렇기에 이 점을 유의해서 사용해야합니다.
@Configuration
public class MultiDataSourceJob {
@Bean
public Step multiDataSourceStep01() {
return stepBuilderFactory.get("multiDataSourceStep01")
.<MainSubDTO, MainSubDTO>chunk(CHUNK_SIZE)
.reader(multiDataSourceBean)
.writer(multiDataSourceBean)
.transactionManager(chainedTransactionManager)
.build()
;
}
}
chunk_size = 1 로 맞췄으며, writer 가 5번 수행된 후, Exception 을 발생시켰습니다.
이렇게 수행한 이유는 같은 트랜잭션 매니저가 동작되는 것을 테스트 하기 위함입니다.
아래와 같이 수행했을 때, 각각 MAIN, SUB DATABASE 에는 5개의 데이터가 쌓입니다.
@RunWith(SpringRunner.class)
@SpringBatchTest
@SpringBootTest(classes={MultiDataSourceJob.class, MultiDataSourceBean.class, TestConfig.class})
public class MultiDataSourceJobIntegrationTest {
@Test(expected = RuntimeException.class)
public void 멀티데이터소스_통합_테스트() throws Exception {
JobExecution jobExecution = jobLauncherTestUtils.launchJob();
Assert.assertThat(jobExecution.getStatus(), is(BatchStatus.COMPLETED));
Assert.assertThat(jobExecution.getExitStatus(), is(ExitStatus.COMPLETED));
}
}
@Slf4j
@Component
@StepScope
@RequiredArgsConstructor
public class MultiDataSourceBean implements ItemReader<MainSubDTO>, ItemWriter<MainSubDTO> {
private final MainRepository mainRepository;
private final SubRepository subRepository;
private int readCount = 10;
private int currentCount = 0;
private int writeCount = 0;
private int stopWriteCount = 5;
@Override
public MainSubDTO read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
if (currentCount == readCount) {
return null;
}
currentCount++;
Main main = Main.builder()
.desc("main desc...")
.build();
Sub sub = Sub.builder()
.etc("sub etc...")
.build();
return MainSubDTO.builder()
.main(main)
.sub(sub)
.build();
}
@Override
public void write(List<? extends MainSubDTO> items) throws Exception {
if (writeCount == stopWriteCount) {
throw new RuntimeException("트랜잭션 테스트");
}
items.forEach(item -> {
mainRepository.save(item.getMain());
subRepository.save(item.getSub());
});
writeCount++;
}
}