Dans divers contextes, les applications peuvent requérir une gestion simultanée de multiples bases de données, par exemple pour répartir des données volumineuses ou pour isoler les opérations de lecture et d'écriture entre des environnements distincts.
La solutino proposée repose sur une extension de Mybatis utilisant un template de session personnalisé. Ce template maintient une correspondance entre des identifiants de bases de données et leurs usines de sessions SQL respectives. L'identifiant actif est conservé dans un ThreadLocal pour garantir la séparation conetxtuelle entre les threads, permettant ainsi à chaque exécution de cibler la source appropriée.
Voici l'implémentation du template dynamique, avec des ajustements de structure et de nommage :
public class DynamicSqlSessionTemplate extends SqlSessionTemplate {
private final SqlSessionFactory defaultFactory;
private final ExecutorType execType;
private final SqlSession proxiedSession;
private final PersistenceExceptionTranslator exTranslator;
private Map<String, SqlSessionFactory> factoryMapping;
private SqlSessionFactory fallbackFactory;
public DynamicSqlSessionTemplate(SqlSessionFactory factory) {
this(factory, factory.getConfiguration().getDefaultExecutorType());
}
public DynamicSqlSessionTemplate(SqlSessionFactory factory, ExecutorType type) {
this(factory, type, new MyBatisExceptionTranslator(
factory.getConfiguration().getEnvironment().getDataSource(), true));
}
public DynamicSqlSessionTemplate(SqlSessionFactory factory, ExecutorType type,
PersistenceExceptionTranslator translator) {
super(factory, type, translator);
this.defaultFactory = factory;
this.execType = type;
this.exTranslator = translator;
this.proxiedSession = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SessionInterceptor());
this.fallbackFactory = factory;
}
public void setFactoryMapping(Map<String, SqlSessionFactory> mapping) {
this.factoryMapping = mapping;
}
public void setFallbackFactory(SqlSessionFactory fallback) {
this.fallbackFactory = fallback;
}
@Override
public SqlSessionFactory getSqlSessionFactory() {
String dbKey = ContextStorage.getDbIdentifier();
SqlSessionFactory selected = factoryMapping.get(dbKey);
if (selected != null) return selected;
if (fallbackFactory != null) return fallbackFactory;
return this.defaultFactory;
}
private class SessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession session = getSqlSession(
DynamicSqlSessionTemplate.this.getSqlSessionFactory(),
DynamicSqlSessionTemplate.this.execType,
DynamicSqlSessionTemplate.this.exTranslator);
try {
Object result = method.invoke(session, args);
if (!isSqlSessionTransactional(session, DynamicSqlSessionTemplate.this.getSqlSessionFactory())) {
session.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (DynamicSqlSessionTemplate.this.exTranslator != null && unwrapped instanceof PersistenceException) {
Throwable translated = DynamicSqlSessionTemplate.this.exTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) unwrapped = translated;
}
throw unwrapped;
} finally {
closeSqlSession(session, DynamicSqlSessionTemplate.this.getSqlSessionFactory());
}
}
}
}
Le contexte de session utilise un stockage local au thread pour gérer l'identifiant actif :
public class ContextStorage {
private static final ThreadLocal<String> dbIdHolder = new InheritableThreadLocal<>();
public static String getDbIdentifier() {
return dbIdHolder.get();
}
public static void setDbIdentifier(String identifier) {
dbIdHolder.set(identifier);
}
public static void resetContext() {
dbIdHolder.remove();
}
}
La configuration initialise les sources de données et assemble le template dynamique :
@Configuration
@MapperScan(basePackages = {"com.example.dao"}, sqlSessionTemplateRef = "dynamicSqlSessionTemplate")
public class MultiDataSourceConfig {
@Autowired
private AppConfig appConfig;
@Bean(name = "dynamicSqlSessionTemplate")
public DynamicSqlSessionTemplate createDynamicTemplate() {
List<DataSourceConfig> configs = appConfig.getDataSourceConfigs();
Map<String, SqlSessionFactory> factoryMap = new HashMap<>();
for (DataSourceConfig cfg : configs) {
SqlSessionFactory factory = initializeFactory(cfg);
if (factory != null) {
factoryMap.put(cfg.getIdentifier(), factory);
}
}
String defaultKey = factoryMap.keySet().iterator().next();
DynamicSqlSessionTemplate template = new DynamicSqlSessionTemplate(factoryMap.get(defaultKey));
template.setFactoryMapping(factoryMap);
return template;
}
private SqlSessionFactory initializeFactory(DataSourceConfig cfg) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(cfg.getJdbcUrl());
dataSource.setUsername(cfg.getUser());
dataSource.setPassword(cfg.getPass());
dataSource.setPoolName(cfg.getIdentifier());
applyPoolSettings(dataSource, cfg.getPoolConfig());
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(cfg.getMapperLocation()));
try {
return bean.getObject();
} catch (Exception e) {
return null;
}
}
private void applyPoolSettings(HikariDataSource ds, PoolSettings settings) {
ds.setDriverClassName(settings.getDriverClass());
ds.setMinimumIdle(settings.getMinIdle());
ds.setMaximumPoolSize(settings.getMaxPool());
ds.setConnectionTimeout(settings.getConnTimeout());
ds.setIdleTimeout(settings.getIdleTimeout());
ds.setMaxLifetime(settings.getMaxLife());
}
}
Les classes de configuration des sources et des paramètres de pool :
public class DataSourceConfig {
private String identifier;
private String jdbcUrl;
private String user;
private String pass;
private String driverClass;
private String mapperLocation;
private PoolSettings poolConfig;
}
public class PoolSettings {
private String driverClass = "com.mysql.cj.jdbc.Driver";
private int minIdle = 5;
private int maxPool = 15;
private long connTimeout = 30000;
private long idleTimeout = 600000;
private long maxLife = 1800000;
}
Notez qu'il est nécessaire de désactiver l'auto-configuration des sources de données au démarrage pour éviter les conflits : @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}).