初めまして
エキサイト株式会社の中です。
最近Javaを書くことが多いので、学んだことを記載していこうと思います。 よろしくお願いいたします。
MyBatisでデータソースを切り変える
弊社では、データベースとJavaを繋ぐlibraryにMyBatisを使用しています。 通常通り使えばそんなに罠はないのだが、 以下のユースケースが現場で発生したので工夫して実装しました。 その例を重要な部分は伏せながら説明します。 ※実装方法の一例であり、現在はもう少しスマートになっております。
ユースケース
話すと長くなるので省略しますが、負荷対策で複数のスキーマにシャーディングしているサービスがあります。 useridごとに分かれているので、MyBatisを使って自動的に振り分けられるようにライブラリーを作成する。
条件
- データベース「スキーマA」「スキーマB」が存在する
- どちらのデータベースも同様のTBLが存在する
- useridが 1000000 - 1999999の場合「スキーマA」、2000000 - 2999999の場合「スキーマB」のユーザ情報が入っている。
- 3000000以上の場合、「スキーマC」が作成される。
コード例
データソース 設定
package com.sample.database; import javax.sql.DataSource; import org.apache.ibatis.scripting.defaults.RawLanguageDriver; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @Configuration public class MySqlSessionFactory { public static final String DATASOURCE_PROPERTY_A = "datasource-property-a"; public static final String DATASOURCE_PROPERTY_B = "datasource-property-b"; /** * 1. * datasourceの設定をapplication.yamlから取得して、bean化して登録する。 * DataSourcePropertiesの型が複数存在する場合、spring側がどれを読むかわからないため、片方に@Primaryをつける */ @Bean(name = {DATASOURCE_PROPERTY_A}) @Primary @ConfigurationProperties(prefix = "spring.datasource.a") public DataSourceProperties propertiesA() { return new DataSourceProperties(); } @Bean(name = {DATASOURCE_PROPERTY_B}) @ConfigurationProperties(prefix = "spring.datasource.b") public DataSourceProperties propertiesB() { return new DataSourceProperties(); } private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject(); sqlSessionFactory.getConfiguration().addMappers("com.sample.database.mapper"); sqlSessionFactory.getConfiguration().addMappers("com.sample.database.mappergen"); sqlSessionFactory.getConfiguration().setCacheEnabled(false); sqlSessionFactory.getConfiguration().setDefaultScriptingLanguage(RawLanguageDriver.class); sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true); return sqlSessionFactory; } public static final String SQL_SESSION_A = "sql-session-a"; /** * 2. * sqlSessionFactoryも同様に、片方に@Primaryをつける * 共通設定は別にprivateメソッド作成する */ @Bean(name = {SQL_SESSION_A}) @Primary public SqlSessionTemplate sqlSessionFactoryA(@Qualifier(MySqlSessionFactory.DATASOURCE_PROPERTY_A) DataSourceProperties properties) throws Exception { final SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(properties.initializeDataSourceBuilder().build()); return new SqlSessionTemplate(sqlSessionFactory); } public static final String SQL_SESSION_B = "sql-session-b"; @Bean(name = {SQL_SESSION_B}) public SqlSessionTemplate sqlSessionFactoryB(@Qualifier(MySqlSessionFactory.DATASOURCE_PROPERTY_B) DataSourceProperties properties) throws Exception { final SqlSessionFactory sqlSessionFactory = createSqlSessionFactory(properties.initializeDataSourceBuilder().build()); return new SqlSessionTemplate(sqlSessionFactory); } }
データソースをapplication.yamlで設定し、sqlSessionFactoryを使ってSqlSessionTemplateを接続先ごとにbean化する。 接続先が増えると、beanが増えていくイメージ。 共通のテーブルが存在するので、mapperは共通のディレクトリを参照する。
データベース切り替え用リゾルバーインターフェース
package com.sample.database; import org.mybatis.spring.SqlSessionTemplate; public interface MySqlSessionTemplateResolver { SqlSessionTemplate getSqlSessionTemplate(long userid); }
データベース切り替え用リゾルバー実体
package com.sample.database; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component public class MySqlSessionTemplateResolverImpl implements MySqlSessionTemplateResolver { private SqlSessionTemplate sqlSessionFactoryA; private SqlSessionTemplate sqlSessionFactoryB; public MySqlSessionTemplateResolverImpl(@Qualifier(MySqlSessionFactory.SQL_SESSION_A) SqlSessionTemplate sqlSessionFactoryBoard, @Qualifier(MySqlSessionFactory.SQL_SESSION_B) SqlSessionTemplate sqlSessionFactoryBoardA ) { this.sqlSessionFactoryBoard = sqlSessionFactoryBoard; this.sqlSessionFactoryBoardA = sqlSessionFactoryBoardA; } @Override public SqlSessionTemplate getSqlSessionTemplate(String userid) { MySqlSessionTemplateType mySqlSessionTemplateType = MySqlSessionTemplateType.getByBlogid(userid); switch(mySqlSessionTemplateType) { case BOARD_A: return this.sqlSessionFactoryBoard; case BOARD_B: return this.sqlSessionFactoryBoardA; default: throw new RuntimeException(); } } }
データベース切り替え用列挙型
package com.sample.database; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; public enum MySqlSessionTemplateType { BOARD_A(1), BOARD_B(2), ; private int prefix; private static Map<Integer, MySqlSessionTemplateType> mySqlSessionTemplateTypes; static { mySqlSessionTemplateTypes = Stream.of(MySqlSessionTemplateType .values()) .collect(Collectors.toMap(f -> f.getPrefix(), f -> f)); } MySqlSessionTemplateType(int prefix) { this.prefix = prefix; } public int getPrefix() { return this.prefix; } public static MySqlSessionTemplateType getByBlogid(String userid) { final String substring = userid.substring(0, 1); MySqlSessionTemplateType type = mySqlSessionTemplateTypes.get(substring); if (Objects.isNull(type)) { throw new RuntimeException(); } return type; } }
リゾルバーから接続先をenumとして登録したクラスからuseridのプレフィックス一桁を取得し、 その接続先を設定する。
使い方
mySqlSessionTemplateResolver
.getSqlSessionTemplate(userid)
.getMapper(UserInfoMapper.class)
.selectOne(~~~~~~~~~~
とMapperを呼び出す前にgetSqlSessionTemplate(userid)で指定すれば、useridによって自動でスキーマの接続先が切り替わります。 SqlSessionTemplateはSpring が管理するトランザクション内で実行され、また Spring によってインジェクトされる複数の Mapper から呼び出すことができるようにスレッドセーフとなっているので try catchで囲む必要もありません。