FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Doma2を使った複数データベースアクセス

こんにちは。フリューのジョンです。

個人的意見ではありますが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を実践利用してバリバリコード書きたいエンジニア募集中です!