動的に接続先のデータベースを切り替える

1リクエスト内で複数のデータベースを検索・更新する必要が生まれたため、その方法について調べました。
**2018/05/20**
**narayanaJTAを使用した記事を[こちら](https://qiita.com/suimyakunosoko/items/afc3f97ffaed78d91adf)に作成しました。**
# 0.概要
### 実現目標
- あらかじめ用意されていた接続情報を使いデータベース接続を行う
- ロジック内で生成された追加情報を付与してデータベース接続を行う

### 環境
- Windows10 Professional
- Java 9.0.4
- PostgreSQL10.3-1
- 使用ライブラリ

```
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-jdbc</artifactId>
		</dependency>
```
# 1.接続情報をyamlファイルに記載し、読み込む
接続情報の取得に使用する`DataSourceProperties.java`、`DataSourcePropertyDetail.java`を作成し、`application.yml`の情報を読み込む

~~~java:DataSourceProperties.java
package com.example.demo.common;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties("demo.datasource")
public class DataSourceProperties {

	/**
	 * 接続情報の初期値を保持
	 */
	private DataSourcePropertyDetail base;

	/**
	 * 接続情報の一覧を保持。不足情報は初期値から設定する
	 */
	private List<DataSourcePropertyDetail> list;

       /** getter setterは省略 */

}
~~~

実際の設定値については`DataSourcePropertyDetail.java`に定義されているものを取得する。
今回はdriverClassName、url、username、password、defaultAutoCommitの最低限の4つを定義する。
また、ロジック内で生成された追加情報を付与する場合を考慮してディープコピー用のコンストラクタを用意する。

~~~java:DataSourcePropertyDetail.java
package com.example.demo.common;

import java.util.Objects;

public class DataSourcePropertyDetail {

	private String name;

	private String driverClassName;
	private String url;
	private String username;
	private String password;
	private Boolean defaultAutoCommit;

	public DataSourcePropertyDetail(){
	}

	public DataSourcePropertyDetail(DataSourcePropertyDetail prop){
		if (Objects.isNull(prop)) {
			return;
		}
		this.driverClassName = prop.driverClassName;
		this.url = prop.url;
		this.username = prop.username;
		this.password = prop.password;
		this.defaultAutoCommit = prop.defaultAutoCommit;
	}

       /** getter setterは省略 */

}

~~~
上記で作成した`DataSourceProperties`、`DataSourcePropertyDetail`が読み込むための情報を`application.yml`に追加します。

~~~YAML:application.yml
demo:
  datasource:
    base:
      driver-class-name: org.postgresql.Driver
      url: jdbc:postgresql://localhost:5432/postgres
      username: postgres
      password: postgres
      default-auto-commit: false
    list:
    - name: postgres
    - name: user
      url: jdbc:postgresql://localhost:5432/user
      username: user
      password: user
    - name: database
      url: jdbc:postgresql://localhost:5432/database
      username: database
      password: database
~~~

#2.実際にデータソースの切り替えを実施する
データソースの切り替えに必要な部品はザックリと２つ。接続情報からデータソースを作成する部品と接続時に使用するデータソースを決定する部品を用意する

##2.1 接続情報からデータソースを作成する部品
基本的に必要な処理は`DataSourceConfig`に記載しても良いが今回は繰り返し使用したい処理もあるため`DataSourceUtil`を用意して今後使用する処理をまとめている。

~~~java:DataSourceConfig.java
package com.example.demo.common;

import java.util.HashMap;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Configuration
public class DataSourceConfig {

	@Autowired
	DataSourceProperties properties;

	@Bean
	@Primary
	public DynamicRoutingDataSourceResolver getRoutingdataSource() {
		DynamicRoutingDataSourceResolver resolver = new DynamicRoutingDataSourceResolver();
		DataSource defDataSource = DataSourceUtil.getDataSource(properties.getBase(), properties.getBase());
		Map<Object, Object> datasources = new HashMap<Object,Object>();
		datasources.put(properties.getBase().getName(), defDataSource);
		properties.getList().forEach(prop -> datasources.put(prop.getName(), getDataSource(prop)));

		resolver.setTargetDataSources(datasources);

		resolver.setDefaultTargetDataSource(defDataSource);

		return resolver;
	}

	private DataSource getDataSource(DataSourcePropertyDetail prop) {
		return DataSourceUtil.getDataSource(properties.getBase(), prop);
	}
}

~~~

~~~java:DataSourceUtil.java
package com.example.demo.common;

import java.util.Objects;
import java.util.function.Consumer;

import org.apache.tomcat.jdbc.pool.DataSource;

public class DataSourceUtil {

	public static DataSource getDataSource(DataSourcePropertyDetail base, DataSourcePropertyDetail prop) {
		DataSource datasource = new org.apache.tomcat.jdbc.pool.DataSource();
		skipIfNull(defaultIfEmpty(prop.getDriverClassName(), base.getDriverClassName()), datasource::setDriverClassName);
		skipIfNull(defaultIfEmpty(prop.getUrl(), base.getUrl()), datasource::setUrl);
		skipIfNull(defaultIfEmpty(prop.getUsername(), base.getUsername()), datasource::setUsername);
		skipIfNull(defaultIfEmpty(prop.getPassword(), base.getPassword()), datasource::setPassword);
		skipIfNull(defaultIfEmpty(prop.getDefaultAutoCommit(), base.getDefaultAutoCommit()), datasource::setDefaultAutoCommit);
		return datasource;
	}

	public static<T> T defaultIfEmpty(T obj, T def) {
		if (Objects.isNull(obj)) {
			return def;
		}

		return obj;
	}

	public static <T> void skipIfNull(T obj, Consumer<T> consumer) {
		if (Objects.isNull(obj)) {
			return;
		}
		consumer.accept(obj);
	}
}
~~~

## 2.2 接続時に使用するデータソースを決定する部品
用意するのは３つ
`AbstractRoutingDataSource`を継承した`DynamicRoutingDataSourceResolver`
ローカルスレッド上に選択されている接続情報を保持する`DataBaseSelectHolder`
接続情報についての構造体となる`DataBaseSelectInfo`

~~~java:DynamicRoutingDataSourceResolver
package com.example.demo.common;

import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicRoutingDataSourceResolver extends AbstractRoutingDataSource {

	@Autowired
	DataSourceProperties properties;

	@Override
	protected Object determineCurrentLookupKey() {
		DataBaseSelectInfo key = DataBaseSelectHolder.getDataBaseInstanceInfo();
		if (Objects.isNull(key)) {
			return null;
		}
		return key.getDataSourceName();
	}

	private DataBaseSelectInfo determineCurrentLookupKeyRaw() {
		return DataBaseSelectHolder.getDataBaseInstanceInfo();
	}

	@Override
	protected DataSource determineTargetDataSource() {
		DataBaseSelectInfo info = determineCurrentLookupKeyRaw();
		// 追加情報があれば追加情報を元にデータソースを作成し
		// 追加情報がなければ継承元クラスのメソッドを呼び出す
		if (info != null && info.isAdditional()) {
			return createNewDataSouce(info);
		}
		return super.determineTargetDataSource();
	}

	// 指定されたプロパティを元に追加情報を付与したデータソースを作成する
	private DataSource createNewDataSouce(DataBaseSelectInfo info) {
		Optional<DataSourcePropertyDetail> op = properties.getList()
			.stream()
			.filter(prop -> Objects.equals(prop.getName(), info.getDataSourceName()))
			.findFirst();
		if (op.isPresent()) {
			DataSourcePropertyDetail prop = new DataSourcePropertyDetail(op.get());

			DataSourceUtil.skipIfNull(info.getAdditionalUrl(),
				 (url) -> prop.setUrl(prop.getUrl() + url));
			DataSourceUtil.skipIfNull(info.getAdditionalUserName(),
				 (name) -> prop.setUsername(prop.getUsername() + name));
			DataSourceUtil.skipIfNull(info.getAdditionalPassword(),
				 (password) -> prop.setPassword(prop.getPassword() + password));

			return DataSourceUtil.getDataSource(properties.getBase(), prop);
		}
		return super.determineTargetDataSource();
	}
}


~~~

~~~java:DataBaseSelectHolder.java

package com.example.demo.common;

import org.springframework.util.Assert;

public class DataBaseSelectHolder {
	 private static ThreadLocal<DataBaseSelectInfo> contextHolder = new ThreadLocal<DataBaseSelectInfo>();

	    public static void setDataBaseInstanceInfo(DataBaseSelectInfo datasource) {
	        Assert.notNull(datasource, "datasource cannot be null.");
	        contextHolder.set(datasource);
	    }

	    public static void setDataBaseInstanceInfo(String dataSourceName) {
	        Assert.notNull(dataSourceName, "dataSourceName cannot be null.");
	        contextHolder.set(new DataBaseSelectInfo(dataSourceName));
	    }

	    public static void setDataBaseInstanceInfo(String dataSourceName, String additionalUrl,
			String additionalUserName, String additionalPassword) {
	        Assert.notNull(dataSourceName, "dataSourceName cannot be null.");
	        contextHolder.set(new DataBaseSelectInfo(dataSourceName, additionalUrl,
			additionalUserName, additionalPassword));
	    }

	    public static DataBaseSelectInfo getDataBaseInstanceInfo() {
	        return contextHolder.get();
	    }

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


~~~

~~~java:DataBaseSelectInfo.java
package com.example.demo.common;

import java.util.Objects;

public class DataBaseSelectInfo {

	private String dataSourceName;

	private String additionalUrl;

	private String additionalUserName;

	private String additionalPassword;

	public DataBaseSelectInfo(String dataSourceName, String additionalUrl
		, String additionalUserName, String additionalPassword) {
		this.dataSourceName = dataSourceName;
		this.additionalUrl = additionalUrl;
		this.additionalUserName = additionalUserName;
		this.additionalPassword = additionalPassword;
	}

       /** getter setterは省略 */

	/**
	 * 追加情報の有無を判定するためのメソッド
	 */
	public boolean isAdditional() {
		return !(Objects.isNull(this.additionalUrl) 
			&& Objects.isNull(this.additionalUserName) 
			&& Objects.isNull(this.additionalPassword));
	}

}

~~~

#3.実際に使用してみる
ControllerとService、DAOを作成して実際に動作させる。
Transactionalのプロパティに現在のトランザクションを一時中断して別トランザクションの作成をするPropagation.REQUIRES_NEWを使用する。

`DemoController#exception`で例外時にロールバックされていることの確認
`DemoController#demo`で複数DBが更新されていることを確認する

~~~java:DemoController.java
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.common.DataBaseSelectHolder;
import com.example.demo.service.DemoService;

@Controller
@Transactional(propagation=Propagation.REQUIRES_NEW)
public class DemoController {

	@Autowired
	private DemoService service;

	@RequestMapping("exception")
	public String exception() {
		DataBaseSelectHolder.setDataBaseInstanceInfo("postgres");
		service.insertException();
		return "empty";
	}

	@RequestMapping("demo")
	public String demo() {
		DataBaseSelectHolder.setDataBaseInstanceInfo("postgres");
		service.insert();
		return "empty";
	}

}
~~~

springのTransactionalの仕様なのか、AOP自体の仕様なのかは不勉強のためわかりませんが、同一インスタンス内ではメソッド呼び出し時にトランザクションが行われないため、トランザクション内でのDB切り替えには次のような方法を使用している。
- DAOクラスのメソッド自体にTransactionalを付与
- 自身のインスタンスを保持して呼び出す
トランザクション単位のクラスを用意したほうが良いような気もするけど今回は諸事情により考えない。

~~~java:DemoService.java
package com.example.demo.service;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.common.DataBaseSelectHolder;
import com.example.demo.dao.DemoDAO;

@Service
@Transactional(propagation=Propagation.REQUIRES_NEW)
public class DemoService {

	@Autowired
	private DemoDAO demoDao;

	@Autowired
	private DemoService self;

	public void insertException() {
		// キー重複エラーでDBがロールバックされる
		DataBaseSelectHolder.setDataBaseInstanceInfo("database");
		List<Map<String, Object>> list = demoDao.selectTransactional();
		list.stream().forEach(demoDao::insert);
		list.stream().forEach(demoDao::insert);
	}

	public void insert() {

		DataBaseSelectHolder.setDataBaseInstanceInfo("user", "001", null, null);
		List<Map<String, Object>> list1 = demoDao.selectTransactional();
		list1.stream().forEach(demoDao::insert);

		DataBaseSelectHolder.setDataBaseInstanceInfo("user", "002", null, null);
		List<Map<String, Object>> list2 = self.getList();
		list2.stream().forEach(demoDao::insert);

		DataBaseSelectHolder.setDataBaseInstanceInfo("database");
		List<Map<String, Object>> list3 = demoDao.select();
		self.insertOther(list3);
	}

	public List<Map<String, Object>> getList() {
		return demoDao.select();
	}

	public void insertOther(List<Map<String, Object>> list) {
		list.stream().forEach(demoDao::insert);
	}

}
~~~

~~~java:DemoDAO.java
package com.example.demo.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Mapper
public interface DemoDAO {

	@Select("SELECT key, value FROM demo")
	List<Map<String, Object>> select();

	@Select("SELECT key, value FROM demo")
	@Transactional(propagation=Propagation.REQUIRES_NEW)
	List<Map<String, Object>> selectTransactional();

	@Insert("INSERT INTO demo (key, value) VALUES ( #{map.key}, #{map.value})")
	int insert(@Param("map")Map<String, Object> map);

	@Update("UPDATE demo SET value = #{map.value} WHERE key = #{map.key}")
	int update(@Param("map")Map<String, Object> map);
}
~~~

## 3.1 DemoController#exception
    http://localhost:8080/exception
~~user01のSELECT結果をpostgresにINSERTするが二回目のINSERTで例外が発生しロールバックされる。~~
**試験データが悪いだけで実際にはロールバックが実施されていない。**
### 実施前DB
- postgres

|key|value|
|:--|:--|
|||


- user01

|key|value|
|:--|:--|
|user001|テストユーザ001|

### 実施後DB
- postgres

|key|value|
|:--|:--|
|||


- user01

|key|value|
|:--|:--|
|user001|テストユーザ001|

## 3.2 DemoController#demo
    http://localhost:8080/demo
user01、user02のSELECT結果をpostgresにINSERT。その後同一トランザクション内でpostgresをSELECTしdatabaseへINSERTする。
### 実施前DB
- postgres

|key|value|
|:--|:--|
|||

- database

|key|value|
|:--|:--|
|database|database|

- user01

|key|value|
|:--|:--|
|user001|テストユーザ001|

- user02

|key|value|
|:--|:--|
|user002|テストユーザ002|

### 実施後DB
- postgres

|key|value|
|:--|:--|
|user001|テストユーザ001|
|user002|テストユーザ002|

- database

|key|value|
|:--|:--|
|database|database|
|user001|テストユーザ001|
|user002|テストユーザ002|

- user01

|key|value|
|:--|:--|
|user001|テストユーザ001|

- user02

|key|value|
|:--|:--|
|user002|テストユーザ002|

#9.参照
[[Spring Boot] 動的データソース変更](https://qiita.com/syukai/items/0d4bf27f82fef9965cdd)

[Spring Bootでトランザクション(@Transactional)の伝搬属性(propagation)を試す](http://pppurple.hatenablog.com/entry/2017/08/30/232405)