mybatisで複数のデータソースを設定する方法について

初めまして

エキサイト株式会社の中です。

最近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で囲む必要もありません。