Spring Boot2 × MyBatis × HikariCPで複数データソースにアクセスするコードを実装する 静的バージョン
前置き
MyBatisを利用した複数データソースの実装には データソースごとのmapperをpackageレベルで分割する静的な手法と、SpringのAbstractRoutingDataSource
を使って都度利用するデータソースを決定する動的な手法があります。今回は静的な手法のコードを記述します。
動的 (透過的) バージョンについてはこちらの記事を参照ください。
ソフトウェアバージョン
software | version |
---|---|
OS | MacOS Mojave |
Spring Boot | 2.1.5 |
Java | 1.8.0_192-b12 |
Kotlin | 1.3.31 |
mybatis-spring-boot-starter | 2.0.1 |
Gradle | 5.2.1 |
実装
複数データソースの場合、Spring BootのAuto ConfigureでBeanを登録することができないので各データソースごとに手作業で登録していきます。登録が必要なものは以下3つ
- DataSource (HikariDataSource)
- TransactionManager (PlatFormTransactionManager)
- SqlSessionFactory (mybatis)
Mapperの配置ディレクトリ
- com.example.demo.infra.mapper.oracle.master (oracleデータソースを利用)
- com.example.demo.infra.mapper.mysql.master (mysqlデータソースを利用)
application.yml
datasource: oracle: master: hikari: jdbc-url: jdbc:oracle:thin:@0.0.0.0:1111/test username: user password: ~ driver-class-name: oracle.jdbc.OracleDriver connection-timeout: 20000 auto-commit: false connection-test-query: SELECT 1 from dual minimum-idle: 0 maximum-pool-size: 1 # jmxで見たい時にtrue register-mbeans: true pool-name: HikariPoolOracle mysql: master: hikari: jdbc-url: jdbc:mysql://0.0.0.0:3306/test username: sa password: ~ driver-class-name: com.mysql.cj.jdbc.Driver connection-timeout: 20000 auto-commit: false connection-test-query: SELECT 1 minimum-idle: 0 maximum-pool-size: 1 connection-init-sql: "SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'" register-mbeans: true pool-name: HikariPoolMysql spring: transaction: rollback-on-commit-failure: true default-timeout: 60s mybatis: mapperLocations: classpath*:mybatis/sql/**/**/*.xml configuration: cacheEnabled: true # prepared statementの再利用をする defaultExecutorType: REUSE # oracle用の設定 jdbcTypeForNull: 'NULL' defaultFetchSize: 100 defaultStatementTimeout: 30 mapUnderScoreToCamelCase: true
設定可能なconfigはHikariCP 参照
また、Mysqlの方はsql_mode を traditional にして、カラムに不正な値を挿入したときにエラーを返すように設定します。参考元
OracleDataSourceConfig.kt
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) @Configuration // mapperScanでoracle用のmapperを置く場所を指定 @MapperScan(basePackages = ["com.example.demo.infra.mapper.oracle.master"], sqlSessionFactoryRef = "oracleMasterSqlSessionFactory") class OracleMasterDatasourceConfig { @Bean(name = [DATA_SOURCE]) @Primary fun oracleMasterDataSource( @Qualifier(HIKARI_CONFIG) hikariConfig: HikariConfig ): DataSource { return HikariDataSource(hikariConfig) } @Bean(name = [HIKARI_CONFIG]) @ConfigurationProperties(prefix = HIKARI_CONFIGURATION_PROPERTIES) fun oracleHikariConfig(): HikariConfig { return HikariConfig() } @Bean(name = [TX_MANAGER]) @Primary fun oracleMasterTxManager( @Qualifier(DATA_SOURCE) dataSource: DataSource, transactionProperties: TransactionProperties ): PlatformTransactionManager { val tx = DataSourceTransactionManager(dataSource) // txのrollbackCommitOnFailureとdefaultTimeOutが設定される // FIXME 設定しなくてもrollbackされたのでやらなくても良い? transactionProperties.customize(tx) return tx } @Bean(name = [SQL_SESSION_FACTORY]) @Primary fun oracleMasterSqlSessionFactory( @Qualifier(DATA_SOURCE) dataSource: DataSource, mybatisProperties: MybatisProperties ): SqlSessionFactory { // val resolver = ResourcePatternUtils.getResourcePatternResolver(DefaultResourceLoader()) val sqlSessionFactoryBean = SqlSessionFactoryBean() sqlSessionFactoryBean.setDataSource(dataSource) sqlSessionFactoryBean.vfs = SpringBootVFS::class.java sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations()) val configuration = org.apache.ibatis.session.Configuration() mybatisProperties.configuration?.let { // oracleに必要 configuration.jdbcTypeForNull = it.jdbcTypeForNull configuration.isCacheEnabled = it.isCacheEnabled configuration.defaultExecutorType = it.defaultExecutorType configuration.defaultFetchSize = it.defaultFetchSize configuration.defaultStatementTimeout = it.defaultStatementTimeout configuration.isMapUnderscoreToCamelCase = true } sqlSessionFactoryBean.setConfiguration(configuration) return sqlSessionFactoryBean.`object` ?: throw NullPointerException() } companion object { private const val HIKARI_CONFIGURATION_PROPERTIES = "datasource.oracle.master.hikari" private const val DATA_SOURCE = "oracleMaster" private const val HIKARI_CONFIG = "oracleMasterHikari" private const val TX_MANAGER = "oracleMasterTxManager" private const val SQL_SESSION_FACTORY = "oracleMasterSqlSessionFactory" } }
必要なBeanを登録します。Mysql側でも同じ型のBeanを登録するため@Primary
を付けています。またメソッド引数に
autowiredする際には@Qualifier
で明示的にBean Idを指定しています。
DataSourceはHikariDataSourceを明示的に返すようにしています。HikariConfigのurlはjdbc-urlという名前なので注意。Spring BootのAutoConfigureではConnection Poolライブラリの実装の差異をaliasを設定することで吸収しているようです。DataSourceBuilder.java
MysqlMasterDataSourceConfig
Oracleの設定から@Primary
を取り除いてconfiguration.jdbcTypeForNull
をデフォルト値にしているだけなのでここでは省略します。
利用
mapper
// https://stackoverflow.com/questions/25379348/idea-inspects-batis-mapper-bean-wrong/34584526 // Repositoryを入れないとIntelliJがautowiredを誤認識する @Repository @Mapper interface ArticleMapper { fun find(): List<ArticleDto> fun update() }
repository
@Repository class ArticleRepositoryImpl( private val articleMapper: ArticleMapper ) : CompanyRepository { override fun update() { articleMapper.update() } override fun find(): List<Article> = articleMapper.find().map { Article( it.title ?: "" ) } }
application (usecase)
@Component class ArticleFacade( private val articleRepository: ArticleRepository ) : Facade { fun find(): Output = Output(articleRepository.find()) // oracleは@PrimaryなのでtransactionManagerは明示しなくてもOK @Transactional(rollbackFor = [Exception::class], transactionManager = "oracleMasterTxManager") fun update() { articleRepository.update() } data class Output( val articles: List<Article> ) }
packageレベルで利用するデータソースが決めてしまっているため特に仕掛けもなく動作します。Exception時のrollbackも正常に機能していました。
動的な手法については設定するclassの数が多いことと複数のSpringのclassの拡張が必要になり、ThreadLocalの考慮も必要と静的な手法に比べて複雑になるため、できるだけ静的な手法を使って実装するほうが良さそうです。
最後に
本番環境で試したソースではないです ;-)
参考サイト
java-handbook/spring at master · tokuhirom/java-handbook · GitHub
How to use HikariCP in Spring Boot with two datasources in conjunction with Flyway - Stack Overflow
mybatis-spring-boot-autoconfigure – MyBatis Sring-BootStarter | Reference Documentation
SpringのDataSourceTransactionManagerを使うとエラー時にCommitされる可能性あり!? - Qiita