背景
モノリシックな管理画面アプリケーションから複数の業務データベースにアクセスする要件が発生し、実行時に接続先を切り替える仕組みが必要になった。マイクロサービス化を見送っているため、単一アプリケーション内で複数DBを操作できるようにする。
依存関係追加
MyBatis-Plus公式が提供する動的データソーススターターを導入する。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
application.yml
spring:
datasource:
dynamic:
primary: master # デフォルトデータソース
strict: false # 存在しないキー指定時に例外を出さない
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://master-db:3306/main?useSSL=false&serverTimezone=Asia/Tokyo
username: ${DB_USER}
password: ${DB_PASS}
sub:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://sub-db:3306/subdb?useSSL=false&serverTimezone=Asia/Tokyo
username: ${DB_USER}
password: ${DB_PASS}
druid:
initial-size: 5
min-idle: 10
max-active: 20
max-wait: 60000
validation-query: SELECT 1
test-while-idle: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
MyBatis設定クラス
@Configuration
public class DynamicMyBatisConfig {
@Autowired
private Environment env;
private static final String CLASS_PATTERN = "**/*.class";
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dynamicDataSource);
factory.setTypeAliasesPackage(resolveTypeAliases());
factory.setMapperLocations(resolveMappers());
factory.setConfigLocation(new DefaultResourceLoader()
.getResource("classpath:mybatis-config.xml"));
factory.setPlugins(new PaginationInterceptor());
return factory.getObject();
}
private String resolveTypeAliases() {
String pkg = env.getProperty("mybatis.type-aliases-package");
if (pkg == null) return null;
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
CachingMetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resolver);
Set<String> packages = new HashSet<>();
for (String p : pkg.split(",")) {
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(p.trim())
+ "/" + CLASS_PATTERN;
try {
for (Resource res : resolver.getResources(pattern)) {
if (res.isReadable()) {
String cls = readerFactory.getMetadataReader(res)
.getClassMetadata().getClassName();
packages.add(Class.forName(cls).getPackage().getName());
}
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("typeAliasesPackage 解決失敗", e);
}
}
return String.join(",", packages);
}
private Resource[] resolveMappers() {
String locations = env.getProperty("mybatis.mapper-locations");
if (locations == null) return new Resource[0];
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
List<Resource> list = new ArrayList<>();
for (String loc : locations.split(",")) {
try {
list.addAll(Arrays.asList(resolver.getResources(loc)));
} catch (IOException ignore) {}
}
return list.toArray(new Resource[0]);
}
}
データソース指定方法
Repository または Service クラスに @DS アノテーションを付与する。Repository に付けることで SQL ファイルも含めてスコープを明確にできる。
@Mapper
@DS("sub")
public interface OrderMapper extends BaseMapper<OrderEntity> {
// この Mapper の全操作は sub データソースへルーティングされる
}
動作確認
- 通常の CRUD は指定したデータソースで実行される。
- トランザション注意:
@Transactionalを Service に付与すると、トランザクション開始時点でデータソースが固定されてしまう。動的切り替えを維持するにはトランザクション境界を見直すか、プログラマティックトランザクションを利用する。 - バッチ処理:
BaseMapperのinsertBatchSomeColumnなどを呼ぶ際にTableInfoが見つからないエラーが出る場合、MybatisSqlSessionFactoryBeanを利用していることを再確認する。