1リクエスト内で複数のデータベースを検索・更新する必要が生まれたため、その方法について調べました。
2018/05/20
narayanaJTAを使用した記事をこちらに作成しました。
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
の情報を読み込む
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つを定義する。
また、ロジック内で生成された追加情報を付与する場合を考慮してディープコピー用のコンストラクタを用意する。
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
に追加します。
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つ。接続情報からデータソースを作成する部品と接続時に使用するデータソースを決定する部品を用意する
##2.1 接続情報からデータソースを作成する部品
基本的に必要な処理はDataSourceConfig
に記載しても良いが今回は繰り返し使用したい処理もあるためDataSourceUtil
を用意して今後使用する処理をまとめている。
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);
}
}
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 接続時に使用するデータソースを決定する部品
用意するのは3つ
AbstractRoutingDataSource
を継承したDynamicRoutingDataSourceResolver
ローカルスレッド上に選択されている接続情報を保持するDataBaseSelectHolder
接続情報についての構造体となるDataBaseSelectInfo
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();
}
}
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();
}
}
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が更新されていることを確認する
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を付与
- 自身のインスタンスを保持して呼び出す
トランザクション単位のクラスを用意したほうが良いような気もするけど今回は諸事情により考えない。
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);
}
}
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] 動的データソース変更