Giriş
Şu satırı dahil ederiz
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
Kısaca
1. AbstractRoutingDataSource sınıfından kalıtan bir bean kodlarız. Bu sınıfta determineCurrentLookupKey() metodunu olmalıdır
2. AbstractRoutingDataSource sınıfından kalıtan bir bean nesnemize setTargetDataSources() ile hedef DataSource nesnelerini atarız.
3. Bu sınıf bir ThreadLocal ile birlikte kullanılır. ThreadLocal.set() ile bir enum veya string verilir. Bu enum veya string'e denk gelen DataSource nesnesi AbstractRoutingDataSource içinde setTargetDataSources() ile atanmıştır.
Açıklaması
şöyle. Yani aslında
AbstractRoutingDataSource sınıfından kalıtsak bile yine bir
DataSource yaratıyoruz.
Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
determineCurrentLookupKey metodu
Açıklaması
şöyle. Bu metod Spring tarafından çağrılır ve hangi DataSource'un kullanılacağını döner. Spring'de kendi içindeki Map'i arayarak ilgili DataSource nesnesini kullanır
A component that extends AbstractRoutingDataSource and is responsible to provide the list of datasources and also to provide the implementation of the determineCurrentLookupKey() method which will help in determining the current datasource.
Örnek
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
// Populate targetDataSources map with tenant's DataSource
// ...
customDataSource.setTargetDataSources(targetDataSources);
return customDataSource;
}
}
In this example, TenantRoutingDataSource extends AbstractRoutingDataSource from Spring and overrides determineCurrentLookupKey() method to provide routing based on tenant context.
Örnek - Enum
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if (AsyncContextHolder.getAsyncContext() != null) {
return AsyncContextHolder.getAsyncContext().get(READTYPE);
}
return null;
}
}
Elimizde şöyle bir enum olsun
public enum ClientDatabase {
CLIENT_A, CLIENT_B
}
DataSourceRouter yaratmak için şöyle
yaparız. Bu nesneye iki tane DataSource atanıyor
@Bean
public DataSource clientDatasource() {
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource clientADatasource = clientADatasource();
DataSource clientBDatasource = clientBDatasource();
targetDataSources.put(ClientDatabase.CLIENT_A,clientADatasource);
targetDataSources.put(ClientDatabase.CLIENT_B, clientBDatasource);
DataSourceRouter datasourceRouter = new ClientDataSourceRouter();
datasourceRouter.setTargetDataSources(targetDataSources);
datasourceRouter.setDefaultTargetDataSource(clientADatasource);
return clientRoutingDatasource;
}
Örnek - Enum
Elimizde şöyle bir kod
olsun. Burada enum içeren ThreadLocal nesne tanımlanıyor
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class DataSourceContextHolder {
private static ThreadLocal<DataSourceEnum> threadLocal;
public DataSourceContextHolder() {
threadLocal = new ThreadLocal<>();
}
public void setDataSourceEnum(DataSourceEnum dataSourceEnum) {
threadLocal.set(dataSourceEnum);
}
public DataSourceEnum getDataSourceEnum() {
return threadLocal.get();
}
public static void clearDataSourceEnum() {
threadLocal.remove();
}
}
Şöyle
yaparız. Burada constructor içinde setTargetDataSources ile her enum'a denk gelen DataSource atanıyor. Ayrıca varsayılan DataSource ta atanıyor.
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
@Component
public class DataSourceRouting extends AbstractRoutingDataSource {
private DataSourceOneConfig dataSourceOneConfig;
private DataSourceTwoConfig dataSourceTwoConfig;
private DataSourceContextHolder dataSourceContextHolder;
public DataSourceRouting(DataSourceContextHolder dataSourceContextHolder,
DataSourceOneConfig dataSourceOneConfig,
DataSourceTwoConfig dataSourceTwoConfig) {
this.dataSourceOneConfig = dataSourceOneConfig;
this.dataSourceTwoConfig = dataSourceTwoConfig;
this.dataSourceContextHolder = dataSourceContextHolder;
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceEnum.DATASOURCE_ONE, dataSourceOneDataSource());
dataSourceMap.put(DataSourceEnum.DATASOURCE_TWO, dataSourceTwoDataSource());
this.setTargetDataSources(dataSourceMap);
this.setDefaultTargetDataSource(dataSourceOneDataSource());
}
@Override
protected Object determineCurrentLookupKey() {
return dataSourceContextHolder.getBranchContext(); //Thread local object
}
}
Controller içinde şöyle
yaparız. Burada ThreadLocal nesneye değer tanıyor
@RestController
@RequiredArgsConstructor
public class DetailsController {
private final DataSourceContextHolder dataSourceContextHolder;
@GetMapping(value="/getEmployeeDetails/{dataSourceType}")
public List<Employee> getAllEmployees(@PathVariable("dataSourceType")
String dataSourceType){
if(DataSourceEnum.DATASOURCE_TWO.toString().equals(dataSourceType)){
dataSourceContextHolder.setBranchContext(DataSourceEnum.DATASOURCE_TWO);
} else {
dataSourceContextHolder.setBranchContext(DataSourceEnum.DATASOURCE_ONE);
}
return employeeService.getAllEmployeeDetails();
}
}
Örnek - AOP + Anotasyon
Şöyle
yaparız. Burada isme sahip iki tane DataSource tanımlanıyor. Bir tanesi varsayılan DataSource
public class AbstractRoutingDataSourceImpl extends AbstractRoutingDataSource {
private static final ThreadLocal<String> DATABASE_NAME = new ThreadLocal<>();
public AbstractRoutingDataSourceImpl(DataSource defaultTargetDatasource,
Map<Object,Object> targetDatasources) {
super.setDefaultTargetDataSource(defaultTargetDatasource);
super.setTargetDataSources(targetDatasources);
super.afterPropertiesSet();
}
public static void setDatabaseName(String key) {
DATABASE_NAME.set(key);
}
public static String getDatabaseName() {
return DATABASE_NAME.get();
}
public static void removeDatabaseName() {
DATABASE_NAME.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return DATABASE_NAME.get();
}
}
@Configuration
@EnableJpaRepositories(basePackages = "com.dynamicdatasource.demo",entityManagerFactoryRef = "entityManager")
public class DynamicDatabaseRouter {
public static final String PROPERTY_PREFIX = "spring.datasource.";
@Autowired
private Environment environment;
@Bean
@Primary
@Scope("prototype")
public AbstractRoutingDataSourceImpl dataSource() {
Map<Object, Object> targetDataSources = getTargetDataSources();
return new AbstractRoutingDataSourceImpl((DataSource)targetDataSources.get("default"), targetDataSources);
}
@Bean(name = "entityManager")
@Scope("prototype")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource()).packages("com.dynamicdatasource").build();
}
private Map<Object,Object> getTargetDataSources() {
//loading the database names to a list from application.properties file
List<String> databaseNames = environment.getProperty("spring.database-names.list",List.class);
Map<Object,Object> targetDataSourceMap = new HashMap<>();
for (String dbName : databaseNames) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(envioronment.getProperty(PROPERTY_PREFIX + dbName + ".driver"));
dataSource.setUrl(environment.getProperty(PROPERTY_PREFIX + dbName + ".url"));
dataSource.setUsername(environment.getProperty(PROPERTY_PREFIX + dbName + ".username"));
dataSource.setPassword(environment.getProperty(PROPERTY_PREFIX + dbName + ".password"));
targetDataSourceMap.put(dbName,dataSource);
}
targetDataSourceMap.put("default",targetDataSourceMap.get(databaseNames.get(0)));
return targetDataSourceMap;
}
}
@Aspect
@Component
@Order(-10)
public class DataSourceAspect {
private final Logger logger = LoggerFactory.getLogger(getClass());
//defininining where the jointpoint need to be applied
@Pointcut("@annotation(com.dynamicdatasource.demo.config.SwitchDataSource)")
public void annotationPointCut() {
}
// setting the lookup key using the annotation passed value
@Before("annotationPointCut()")
public void before(JoinPoint joinPoint){
MethodSignature sign = (MethodSignature)joinPoint.getSignature();
Method method = sign.getMethod();
SwitchDataSource annotation = method.getAnnotation(SwitchDataSource.class);
if(annotation != null){
AbstractRoutingDataSourceImpl.setDatabaseName(annotation.value());
logger.info("Switch DataSource to [{}] in Method [{}]",
annotation.value(), joinPoint.getSignature());
}
}
// restoring to default datasource after the execution of the method
@After("annotationPointCut()")
public void after(JoinPoint point){
if(null != AbstractRoutingDataSourceImpl.getDatabaseName()) {
AbstractRoutingDataSourceImpl.removeDatabaseName();
}
}
}
Aspect için anotasyon şöyle olsun
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchDataSource {
String value() default "";
}
Kullanırken şöyle yaparız
@SwitchDataSource(value = "college")
public List<College> getAllColleges(){
return collegeRepository.findAll();
}
@SwitchDataSource(value = "student")
public List<Student> getAllStudents(){
return studentRepository.findAll();
}
Örnek - TransactionSynchronizationManagerBurada amaç @Transactional ve @Transactional(readOnly = true) olarak işaretli çağrıları farklı veri tabanlarına göndermek. Şeklen
şöylepublic class TransactionRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ?
DataSourceType.READ_ONLY :
DataSourceType.READ_WRITE;
}
}
Bu nesneyi yaratmak ve doldurmak için şöyle
yaparızpublic class TransactionRoutingConfiguration
extends AbstractJPAConfiguration {
...
@Bean
public DataSource readWriteDataSource() {
...
}
@Bean
public DataSource readOnlyDataSource() {
...
}
@Bean
public TransactionRoutingDataSource actualDataSource() {
TransactionRoutingDataSource routingDataSource =
new TransactionRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(
DataSourceType.READ_WRITE,
readWriteDataSource()
);
dataSourceMap.put(
DataSourceType.READ_ONLY,
readOnlyDataSource()
);
routingDataSource.setTargetDataSources(dataSourceMap);
return routingDataSource;
}
}