LoginSignup
5
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-25

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.javaDataSourcePropertyDetail.javaを作成し、application.ymlの情報を読み込む

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つを定義する。
また、ロジック内で生成された追加情報を付与する場合を考慮してディープコピー用のコンストラクタを用意する。

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は省略 */

}

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

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を用意して今後使用する処理をまとめている。

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);
    }
}

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 接続時に使用するデータソースを決定する部品

用意するのは3つ
AbstractRoutingDataSourceを継承したDynamicRoutingDataSourceResolver
ローカルスレッド上に選択されている接続情報を保持するDataBaseSelectHolder
接続情報についての構造体となるDataBaseSelectInfo

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();
    }
}


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();
        }
}


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が更新されていることを確認する

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を付与
- 自身のインスタンスを保持して呼び出す
トランザクション単位のクラスを用意したほうが良いような気もするけど今回は諸事情により考えない。

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);
    }

}
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] 動的データソース変更

Spring Bootでトランザクション(@Transactional)の伝搬属性(propagation)を試す

5
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
13