Spring BootアプリケーションでMyBatis-Plusを使った動的データソース切り替え実装

背景

モノリシックな管理画面アプリケーションから複数の業務データベースにアクセスする要件が発生し、実行時に接続先を切り替える仕組みが必要になった。マイクロサービス化を見送っているため、単一アプリケーション内で複数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 に付与すると、トランザクション開始時点でデータソースが固定されてしまう。動的切り替えを維持するにはトランザクション境界を見直すか、プログラマティックトランザクションを利用する。
  • バッチ処理BaseMapperinsertBatchSomeColumn などを呼ぶ際に TableInfo が見つからないエラーが出る場合、MybatisSqlSessionFactoryBean を利用していることを再確認する。

タグ: MyBatis-Plus Dynamic DataSource Spring Boot Druid MyBatis

5月20日 04:47 投稿