こんにちは。フリューのジョンです。
個人的意見ではありますがSpring Data JPAのほうが好きですが、今回はDoma2を使った複数データベースアクセスを実装して躓いたので書かせていただきたいと思います。
概要
環境としましては以下のようになります。
| spring-boot | 1.5.3.RELEASE |
| doma-spring-boot-starter | 1.1.0 |
| spring-boot-starter-aop | 1.5.4.RELEASE |
複数のデータベースアクセスの具体的な要件は以下になります。
- データベースアクセスをするメソッドに対して、アノテーションを付けてアクセス先を切り替える
- アノテーションを付けていない場合はデフォルトのデータベースにアクセスする
- 接続先はDomaの基本に合わせて、DataSourceの切り替えをしたい
spring:
datasource:
default:
url: defaultUrl
username: default
password: default
driver-class-name: oracle.jdbc.OracleDriver
test:
url: testUrl
username: test
password: test
driver-class-name: oracle.jdbc.OracleDriver
product:
url: prodUrl
username: prod
password: prod
driver-class-name: oracle.jdbc.OracleDriver
以上の要件を満たすために、AbstractRoutingDataSourceとAnnotation、ThreadLocalを利用しました。
実装については、Qiitaの「Spring Bootで複数データベースを扱うウェブアプリケーションのサンプル」の記事を参考にさせていただきました。
実装
Daoには実装は加えません。
肝になるのは、AbstractRoutingDataSourceです。AbstractRoutingDataSourceはSpringのJDBCパッケージの中にあります。
このクラスを実装したモノを、Domaのconfigを継承したConfigrationクラスが返却することで対応が可能です。
@Configuration
public class AppConfig implements Config {
@Override
public Dialect getDialect() {
return new OracleDialect();
}
@Override
@Bean
public DataSource getDataSource() {
AbstractRoutingDataSource abstractRoutingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
if (MultiDataSourceContextHolder.getDataSourceType() == null) {
return DataSourceType.DEFAULT.getName(); //アノテーションがセットされていなければデフォルトのデータソースを使う
}
return MultiDataSourceContextHolder.getDataSourceType().getName();
}
};
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put(DataSourceType.PRODUCT.getName(), dataSourceForProduct());
dataSources.put(DataSourceType.TEST.getName(), dataSourceForTest());
dataSources.put(DataSourceType.DEFAULT.getName(), dataSourceForDefault());
abstractRoutingDataSource.setTargetDataSources(dataSources);
abstractRoutingDataSource.setDefaultTargetDataSource(dataSourceForDefault());
return abstractRoutingDataSource;
}
@ConfigurationProperties(prefix = "spring.datasource.product")
@Bean("dataSourceForProduct")
public DataSource dataSourceForProduct() {
return DataSourceBuilder.create().build();
}
@ConfigurationProperties(prefix = "spring.datasource.test")
@Bean("dataSourceForTest")
public DataSource dataSourceForTest() {
return DataSourceBuilder.create().build();
}
@ConfigurationProperties(prefix = "spring.datasource.default")
@Bean("dataSourceForDefault")
@Primary //これがないと起動できません
public DataSource dataSourceForDefault() {
return DataSourceBuilder.create().build();
}
}
ここで、AppConfigはルートである必要があります。(もしかすると、そうでなくてもいける方法があるのかもしれませんが、私が調べた限り出来ませんでした……)
接続先を保存するためにstaticでデータを作っておきます。ThreadLocalですので取扱には注意です。
public class MultiDataSourceContextHolder {
private static ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType dataSourceType) {
if (dataSourceType == null) {
throw new NullPointerException();
}
contextHolder.set(dataSourceType);
}
public static DataSourceType getDataSourceType() {
return contextHolder.get();
}
public static void clearTenantType() {
contextHolder.remove();
}
}
メソッドに必要なアノテーションは以下のように設定します
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchingDataSource {
DataSourceType value() default DataSourceType.TEST;
}
アノテーションの実装は以下になります。
アノテーションがあるメソッドが動く前に接続先をHolderにセットして、動作した後に開放しています。
@Order(99)
@Aspect
@Component
public class SwitchingDataSourceAop {
@Around("@annotation(swds)")
public Object switchingForMethod(ProceedingJoinPoint pjp, SwitchingDataSource swds) throws Throwable {
try {
MultiDataSourceContextHolder.setDataSourceType(swds.value());
return pjp.proceed();
} finally {
MultiDataSourceContextHolder.clearTenantType();
}
}
}
接続先はenumで持つようにします。
@AllArgsConstructor
@Getter
public enum DataSourceType {
DEFAULT("defaultDatasource"),
PRODUCT("productDatasource"),
TEST("testDatasource");
private final String name;
}
ここまでで実装終わりです。
使い方
以下のようになります。
@Controller
@RequestMapping("/")
public class TestController {
@Resource
private DBAccessService dbAccessService;
@ResponseBody
@GetMapping
public ResponseEntity test() {
dbAccessService.test();
dbAccessService.prod();
dbAccessService.defo();
return ResponseEntity.ok().build();
}
}
@Service
public class DBAccessServiceImpl {
@Override
@SwitchingDataSource(DataSourceType.TEST)
public void test() {
// DBアクセスするなんとか
}
@Override
@SwitchingDataSource(DataSourceType.PRODUCT)
public void prod() {
// DBアクセスするなんとか
}
@Override
public void defo() {
// DBアクセスするなんとか
}
}
まとめ
今回はアノテーションを使った振り分け方を行いましたが、リクエストパラメータやCookieを使って振り分けるというのも良いかと思います。
Doma2がConfigを勝手に参照してくれるのは、とても楽ですね。設定だけでうまくいきました。
最後に
弊社は、Springを実践利用してバリバリコード書きたいエンジニア募集中です!