SpringBootで動的にDBを切り替えてみる

# はじめに
[ユアマイスターアドベントカレンダー2019](https://qiita.com/advent-calendar/2019/yourmystar) の7日目の記事です。

今年の春から新卒のエンジニアとして働き始めました。

就職した会社では主に、**Java**と**SpringBoot**を利用して開発を行なっています。

携わった開発の中で、`1アプリケーションで本番環境とステージング環境の2つのDBに接続して、メソッド単位で接続するDBを動的に切り替えるということをやりたい`という要件があり、それを満たすためにどのような実装が必要かを調査し、実際に実装を行うことになりました。

## 概要
### 今回やりたいこと
- 複数のデータベースに接続する
- 動的に接続先のDBを切り替える

### 環境
- SpringBoot(バージョンは2.2.1)
- Java 8
- JPA

を利用しています。

### 接続するデータベース
- Prod(本番用)
- Stg(ステージング環境)

の2つがあります。

### 今回の要件
特定のメソッドを実行するときのみ、DBの切り替えを行いたい

## 参考にしたもの
- [[Spring Boot] 動的データソース変更]([Spring Boot] 動的データソース変更)

上記の記事を参考にしました。
こちらでは、interceptorを利用してエントリーポイント(Controllerのメソッド)ごとにDBを切り替える処理を行なっていましたが、今回は、Service層でDBの切り替えを行いたかったためAOPを利用して実現しました。

## 実際にやってみる
### DataSourceの接続を定義するconfigクラスを作成する
```java:DataSourceConfig.java
@Component
@Configuration
public class DataSourceConfig {

  private final Environment environment;

  public DataSourceConfig(Environment environment) {
    this.environment = environment;
  }

  /**
   * stg用のdataSource.
   *
   * @return stgのdataSource
   */
  @Bean
  public HikariDataSource stgDataSource() {
    String baseConfig = "spring.datasource.stg.%s";
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(environment.getProperty(String.format(baseConfig, "url")));
    config.setUsername(environment.getProperty(String.format(baseConfig, "username")));
    config.setPassword(environment.getProperty(String.format(baseConfig, "password")));
    return new HikariDataSource(config);
  }

  /**
   * 本番用のdataSource.
   *
   * @return 本番用のdataSource
   */
  @Bean
  public HikariDataSource prodDataSource() {
    String baseConfig = "spring.datasource.prod.%s";
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(environment.getProperty(String.format(baseConfig, "url")));
    config.setUsername(environment.getProperty(String.format(baseConfig, "username")));
    config.setPassword(environment.getProperty(String.format(baseConfig, "password")));
    return new HikariDataSource(config);
  }

  /**
   * dataSourceのdefaultを設定するBean.
   *
   * @return DynamicRoutingDataSourceResolver
   */
  @Bean
  @Primary
  public DynamicRoutingDataSourceResolver dataSourceResolver() {
    DynamicRoutingDataSourceResolver resolver = new DynamicRoutingDataSourceResolver();

    Map<Object, Object> dataSources = new LinkedHashMap<>();
    dataSources.put("stg", stgDataSource());
    dataSources.put("prod", prodDataSource());

    resolver.setTargetDataSources(dataSources);
    resolver.setDefaultTargetDataSource(stgDataSource());

    return resolver;
  }
}

```

このクラスで、DBの接続情報をapplication.ymlから取得します。

`DynamicRoutingDataSourceResolver`というクラスで、どのDBに接続を行うのかという設定を行います。
今回は、デフォルトはステージングのDBに接続したいので

```java
/**
   * dataSourceのdefaultを設定するBean.
   *
   * @return DynamicRoutingDataSourceResolver
   */
  @Bean
  @Primary
  public DynamicRoutingDataSourceResolver dataSourceResolver() {
    DynamicRoutingDataSourceResolver resolver = new DynamicRoutingDataSourceResolver();

    Map<Object, Object> dataSources = new LinkedHashMap<>();
    dataSources.put("stg", stgDataSource());
    dataSources.put("prod", prodDataSource());

    resolver.setTargetDataSources(dataSources);
    resolver.setDefaultTargetDataSource(stgDataSource());

    return resolver;
  }
```

この部分で`stgDataSource()`を呼び出し、デフォルトの接続先として定義します。

### DynamicRoutingDataSourceResolverを作る

```java:DynamicRoutingDataSourceResolver.java
/**
 * 条件に応じてdataSourceを切り替えるクラス.
 */
public class DynamicRoutingDataSourceResolver extends AbstractRoutingDataSource {

  /**
   * 使用するdataSourceのキーを設定するメソッド.
   *
   * @return String 接続先のdataSourceを表す文字列
   */
  @Override
  protected Object determineCurrentLookupKey() {

    if (DataSourceContextHolder.getDataSourceType() == DataSourceType.STG) {
      return "stg";
    } else if (DataSourceContextHolder.getDataSourceType() == DataSourceType.PROD) {
      return "prod";
    }

    // デフォルトはstgを返却
    return "stg";
  }
}
```

SpringでDBの接続先を決定する`AbstractRoutingDataSource`を継承したクラスを作成します。
通常だと、`application.yml`に記述された接続情報を元に、接続を行います。

今回は動的に切り替えたいので。アプリケーションのスレッドに書き込まれた情報をもとにDBを切り替えようと思います。

### DataSourceContextHolderの作成
```java:DataSourceContextHolder.java
/**
 * アプリケーションのスレッドごとにdataSourceの情報を持つクラス.
 */
public class DataSourceContextHolder {
  private static ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();

  public static void setDataSourceType(DataSourceType dataSourceType) {
    contextHolder.set(dataSourceType);
  }

  public static DataSourceType getDataSourceType() {
    return contextHolder.get();
  }

  public static void clear() {
    contextHolder.remove();
  }
}
```

アプリケーションのスレッドごと(１リクエストごと)にDBの接続先を切り替えるために、`ThreadLocal`を利用します。
ここにDBのSTGかPRODかをsetし、先ほど作成した`DynamicRoutingDataSourceResolver.java`でどちらを利用するかを定義して、DBを動的に切り替えます。


### 接続するDataSourceの種類を定義するEnumの作成

```java:DataSource.java

/**
 * dataSourceを変更するときに用いるアノテーション.
 */
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

  /**
   * defaultはSTG.
   *
   * @return enumで定義したdatasource
   */
  DataSourceType value() default DataSourceType.STG;
  enum DataSourceType {
    STG,
    PROD
  }
}
```

見たままですが、今回のDBの情報を定義したEnumを作成します。

### DataSourceAdvice(AOP)の作成

```java:DataSourceAdvice.java
@Aspect
@Component
public class DataSourceAdvice {

  @Around("@annotation(dataSource)")
  public void adviceMethod(ProceedingJoinPoint proceedingJoinPoint, DataSource dataSource)
      throws Throwable {
    DataSourceContextHolder.setDataSourceType(dataSource.value());
    proceedingJoinPoint.proceed();
  }
}
```

今回はDBを切り替えるメソッドにアノテーションを付与して、動的にDBを切り替えるようにしました。
DBを切り替えたいメソッドの直前に、`@DataSource(value = DataSourceType.PROD)`のアノテーションを付与した場合にのみ本番のDBに接続されるようにしました。

AOPについてですが、ざっくりと書くと`@Aspect`のアノテーションを付与したクラスはアプリケーション内で行う共通処理を実装するというものです。よく使われるものとしてはログ出力などが挙げられます。

<img width="1054" alt="プレゼンテーション1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221678/12eb542b-db61-0a83-30f9-3006b3df747c.png">


簡単な図ですが、今回`@DataSource`のアノテーションを付与したメソッドに図のオレンジ色の部分のように割り込み処理を発生させます。
割り込ませる処理はDBの接続先を任意のものに変更する`DynamicRoutingDataSourceResolver`の`determineCurrentLookupKey()`メソッドになります。
この処理によりDBを切り替えることができます。


### 動作させるアプリケーション
https://github.com/Masaki-Ogawa/datasourceDemo

今回作成したソースコード

## 終わりに
Laravelなら数行で終わるものをSpringBootでやろうとするとここまでかかってしまうのかと個人的には思いました。
(あまりニーズのないパターンなのかなと思います。)
実装している間にもLaravelなら・・・・と思うシーンがたくさんありました。
逆にアノテーションの機能の多彩さはSpringBootを触れて感動したので

それぞれのフレームワークや言語でいいところ、悪いところが見えてくるので、複数の技術の触れるのは重要だなと思いました。



参考までに記事を引用させていただきます。
[[Laravel]Laravelで別々のDB（MySQL）に接続させる方法](https://qiita.com/qiita-kurara/items/2e3efb4ccbb2489b05c3)