データソースの動的切り替え
ユーザさんから、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環境でのデータソースの動的切り替えは結構簡単ですね。