データソースの動的切り替え

ユーザさんから、Webアプリ起動中にデータソースを動的に切り替えたい、との要望があったのでSpring環境でどうやるかを調べてみました。

データソースの定義

    <!-- 動的切り替え用リゾルバ -->
    <bean id="dataSource"
        class="net.sasuke.datasource.DynamicRoutingDataSourceResolver">
        <property name="targetDataSources">
            <map key-type="net.sasuke.datasource.type.SchemaType">
                <entry key="MANAGER" value-ref="managerDataSource" />
                <entry key="USER" value-ref="userDataSource" />
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="userDataSource" />
    </bean>

    <!-- データソースの共通設定 -->
    <bean id="abstractDataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource"
        abstract="true">
        <property name="driverClassName" value="${database.driver}" />
        <property name="username" value="${database.user}" />
        <property name="password" value="${database.password}" />
    </bean>
    
    <bean id="managerDataSource" parent="abstractDataSource">
        <property name="url" value="${database.url.manager}" />
    </bean>

    <bean id="userDataSource" parent="abstractDataSource">
        <property name="url" value="${database.url.user}" />
    </bean>

上記の例では、データソースは2つあり、1つは管理者向けデータソース(managerDataSource)、もう1つはユーザ向けデータソース(userDataSource)として定義しています。2つのデータソースはURL以外は共通の設定となっているので、abstractDataSourceとして共通部分の抽象化を行っています。

DynamicRoutingDataSourceResolver

DynamicRoutingDataSourceResolverは、AbstractRoutingDataSourceのサブクラスで、切り替え対象のデータソースをtargetDataSourcesとしてMap形式で保持します。defaultTargetDataSourceはデフォルトで利用するデータソースを定義します。次にDynamicRoutingDataSourceResolverの中身です。

public class DynamicRoutingDataSourceResolver extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return SchemaContextHolder.getSchemaType();
    }
}

DynamicRoutingDataSourceResolverでは、SchemaContextHolderで保持しているSchemaTypeを返却するだけとなります。このとき返却されるSchemaTypeがnullの場合、defaultTargetDataSourceで指定したデータソースが利用されます。また、SchemaTypeがnullでない場合も、targetDataSourcesで指定されたMapに該当するキーが存在しない場合もdefaultTargetDataSourceが利用されます。

SchemaContextHolder

public class SchemaContextHolder {
    private static ThreadLocal<SchemaType> contextHolder = new ThreadLocal<SchemaType>();
    /**
     * スレッドローカルに対象スキーマを設定します。
     * 
     * @param type
     *            対象スキーマの種類
     */
    public static void setSchemaType(SchemaType type) {
        Assert.notNull(type, "Schema type cannot be null.");
        contextHolder.set(type);
    }
    /**
     * 対象スキーマを返却します。
     * 
     * @return 対象スキーマ
     */
    public static SchemaType getSchemaType() {
        return contextHolder.get();
    }
    /**
     * スレッドローカルを空にします。
     */
    public static void clear() {
        contextHolder.remove();
    }
}

setSchemaTypeで指定されたSchemaTypeをThreadLocalに保存します。

SchemaType

enumでtargetDataSourcesのキーを宣言します。

public enum SchemaType {
    /** ユーザ向けデータソースのキーです。 */
    USER,
    /** 管理者向けデータソースのキーです。 */
    MANAGER
}

使い方

Springではトランザクションを開始するタイミングでデータソースを決定してgetConnection()を実行しているようです。(コネクションプーリングを行っていない場合)。いま自分が携わっているシステムの場合、Serviceクラスに@Transactionalを指定しているため、ControllerがServiceクラスのメソッドを呼び出す前にSchemaContextHolderにSchemaTypeを設定する必要があります。

@Controller
@RequestMapping(value = "/hoge")
public class HogeController extends AbstractController {

    @Autowired
    private HogeService hogeService;

    @RequestMapping(value = "search", method = RequestMethod.POST)
    public String search(HogeCommand command, Model model) {
        if ("user".equals(command.getLoginType())) {
            SchemaContextHolder.setSchemaType(SchemaType.USER);
        } else if ("manager".equals(command.getLoginType())) {
            SchemaContextHolder.setSchemaType(SchemaType.MANAGER);
        }
        model.addAttribute("hogeItems", hogeService.findByCondition(command));
        return Page.HOGE;
    }
}

こんな感じでSpring環境でのデータソースの動的切り替えは結構簡単ですね。