はじめに
こんにちは。
先日、【超初心者向け】Spring Boot超入門(WEBアプリ編)という記事を書きましたが、今回はその発展編です。
JavaでのWEBアプリケーション開発でとても強力なフレームワーク、Spring Bootを、より便利に使って開発するワザを集めました。
先日、業務でテストデータを投入するツールを作ろうとしたときに集めておいたノウハウを、こちらにメモを残すと同時に、誰かのお役に立てるかと思い、記事の執筆に至りました。
今回のテーマ
こちらが今回のテーマです。
Spring関係ないやん。と云うのも混じっていますが、筆者のメモですのでご容赦ください。
- 画面をパーツごとに分割して管理する
- 条件によって表示する内容を変える
- ドロップダウンとかの要素を動的に設定する
- CSSネスト
- 使うデータソース(DB)をアプリケーション中に動的に切り替える
これらテーマで、以降それぞれやり方を解説していきます。
今回はプロジェクトの構成方法や、実行イメージなどは事細かに載せませんので、それから見たい、知りたい方は、冒頭のリンクにある記事をご覧ください。
プロジェクトツリー
今回使ったサンプルプロジェクトのツリーを紹介しておきます。
すべてのソースコードを説明するわけではありませんが、大体のイメージに置いておきます。
プロジェクトルート
└─src
├─main
│ ├─java
│ │ └─jp
│ │ └─co
│ │ └─taro
│ │ │ TestDataMakerApplication.java
│ │ │
│ │ ├─config
│ │ │ DataSourceConfig.java
│ │ │ DBListConfig.java
│ │ │ MyBatisConfig.java
│ │ │
│ │ ├─controller
│ │ │ TestDataMakerController.java
│ │ │
│ │ ├─datasource
│ │ │ DatabaseContextHolder.java
│ │ │ RoutingDataSource.java
│ │ │
│ │ ├─entity
│ │ │ UserTable.java
│ │ │
│ │ ├─mapper
│ │ │ UserTableMapper.java
│ │ │
│ │ ├─model
│ │ │ TestDataMakerModel.java
│ │ │
│ │ ├─service
│ │ │ TestDataMakerService.java
│ │ │
│ │ └─share
│ │ TargetDB.java
│ │
│ └─resources
│ │ application.yml
│ │ logback-spring.xml
│ │
│ ├─jp
│ │ └─co
│ │ └─taro
│ │ └─mapper
│ │ UserTableMapper.xml
│ │
│ ├─static
│ │ │ index.html
│ │ │
│ │ └─css
│ │ base.css
│ │
│ ├─templates
│ │ │ TestDataMaker.html
│ │ │
│ │ └─parts
│ │ HeaderMenu.html
│ │ LeftMenu.html
│ │
│ └─tns
│ tnsnames.ora
│
└─test
└─java
└─jp
└─co
└─taro
TestDataMakerApplicationTests.java
画面をパーツごとに分割して管理する
さっそくテーマの1つ目です。
WEBアプリだけに限った話ではありませんが、複数の画面を作るとき、各画面にお決まりのものを置きたいことって、結構ありますよね。
例えば、こんな感じです。
よく見る感じの、上下左右がお決まりのエリアになっていて、真ん中のコンテンツだけを書き換えたい。
と云うレイアウトです。
こんなとき、上下左右のメニューやらなんやらを毎回書きたくないです。
と云うか、この手のものはパーツごとに管理するのが当たり前です。
それをThymeleafでやります。
方法はいくつかあり、それ用のライブラリもあるようですが、今回はThymeleafの機能のみで実現します。
やり方は簡単で、th:insert
と云うタグのパラメータに、読み込みたいファイルを指定するだけです。
今回のツリー上、メインのコンテンツはTestDataMaker.html
で、パーツがHeaderMenu.html
とLeftMenu.html
です。
それらを、読み込むにはこうなります。
<!-- ヘッダー始まり -->
<div th:insert="~{parts/HeaderMenu :: header}" />
<!-- ヘッダー終わり -->
<!-- 左のメニュー始まり -->
<div th:insert="~{parts/LeftMenu :: leftmenu}" />
<!-- 左のメニュー終わり -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="/css/base.css" rel="stylesheet" type="text/css">
</head>
<body>
<!--
ヘッダーのエリアを定義する。
タグ名は適当でよく、参照する側では「th:fragment」で指定した名前を指定する。
-->
<header th:fragment="header">
<div id="header">
<table border="0" align="center" cellpadding="0" width="1100">
<tr>
<td><div id="table-left"><h1>TEST DATA MAKER</h1></div></td>
</tr>
</table>
</div>
<div id="header-menu">
<ul>
<li><a href="TestDataMaker.html">テストデータ作成</a></li>
<li><a href="TestDataMaker.html">リンク</a></li>
</ul>
</div>
</header>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link th:href="/css/base.css" rel="stylesheet" type="text/css">
</head>
<body>
<leftmenu th:fragment="leftmenu">
<div id="menu">
<div class="menulist">
<h2>コンテンツ</h2>
<a href="TestDataMaker.html">コンテンツ1</a>
<a href="TestDataMaker.html">コンテンツ2</a>
</div>
</div>
</leftmenu>
</body>
</html>
ソースコード全文は後程載せますので、ここではエッセンスだけです。
前述のinsert
タグのパラメータに、クラスパスのルートから/
、拡張子なしのファイル名を指定して、::
のあとに、読み込むパーツ側で指定している名前を指定ます。
これだけで、パーツの読み込みは簡単にできます。
条件によって表示する内容を変える
続いてのテーマはこちらです。
表示する内容と言っても、メッセージとか検索結果とか、要素の中身を変えるのではなく、要素ごと表示したり、させないやり方です。
例えば最初の入力によって、後続で入力させるテキストボックスの数が増減するときなどに使います。
<th:block th:if="${#strings.isEmpty(tdmm.targetDB)}">
formのtdmm.targetDBが空の場合に表示する
</th:block>
<th:block th:if="${!#strings.isEmpty(tdmm.targetDB)}">
formのtdmm.targetDBが空でない場合に表示する
</th:block>
package jp.co.taro.model;
import java.util.List;
import jp.co.taro.entity.UserTable;
import lombok.Data;
/**
* <h1>[テストデータ作成ツールのモデル]</h1></br>
*<br>
* 画面のformの内容を持つ。
*/
@Data
public class TestDataMakerModel {
/** DB選択のドロップダウンのリスト */
private List<String> dbList;
/** 選択した対象のDB */
private String targetDB;
/** 現在DBにあるデータのリスト */
private List<UserTable> dataList;
}
このように、th:block
のタグに、th:if
で条件を指定することにより、th:block
ごと表示、非表示を切り替えることができます。
先のサンプルでは、フォーム(Model)のtdmm.targetDB
の内容が空か、そうでないかで表示、非表示を制御しています。
ちなみに、サンプルで使っているデータはString
型ですが、String型
でないときもstrings.isEmpty()
で空かそうでないかを判断できます。
ドロップダウンとかの要素を動的に設定する
ドロップダウン、ラジオボタンなど、選択肢の中身を動的に書き換えたい場合のやり方です。
正直、あまり使わないと云うか、動的にしてもそんな変わらないところもありますが、知っていて損はない。
と云うことで紹介しておきます。
今回はドロップダウンの中身をサンプルに使います。
これは結構面倒で、XMLとかにリストを作っといて、それを読んだらできないかなー?とか思っていたのですが、Controllerかどこかに、自力でModelに選択肢をセットしてあげないとダメのようです。
<select id="targetDB" th:field="*{targetDB}">
<!-- ModelのListの中身をループで表示させる -->
<option th:each="db : ${tdmm.dbList}" th:value="${db}" th:text="${db}" />
</select>
package jp.co.taro.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* <h1>[DBのリスト]</h1></br>
*/
@Configuration
public class DBListConfig {
/**
* <h2>[DBのリスト]</h2></br>
*<br>
* ドロップダウンに使用する。
*/
@Bean
List<String> dbList() {
return new ArrayList<String>() {
{
add("UT1");
add("IT1");
}
};
}
}
public class TestDataMakerController {
/** DBのリスト */
@Autowired
private List<String> dbList;
/**
* <h2>[初期処理]</h2></br>
*<br>
* @param model テストデータ作成ツールのモデル
* @return 遷移先(トップ)
*/
@GetMapping("/TestDataMaker")
private String init(Model model) {
// --------------------------------------------------------------------
// ドロップダウンのリストを作成
// --------------------------------------------------------------------
TestDataMakerModel tdmm = new TestDataMakerModel();
tdmm.setDbList(dbList);
// --------------------------------------------------------------------
// 生成したオブジェクトをModelに追加
// --------------------------------------------------------------------
model.addAttribute("tdmm", tdmm);
return "TestDataMaker";
}
サンプルだと、JavaConfigというのでしょうか?@Configuration
を付加したクラスのBeanとして、ドロップダウンのリストを定義して、それをControllerでModelに追加したものを表示しています。
余談ですが、最近はこういうちょっとしたものも@Configuration
を使うのが主流のようですね。
Spring Bootの機能を有効活用するにはこうなるんだと思いますが、この類のものをJavaの中身に書いてしまうと、書き換えた時にビルドせざるを得ないので、個人的にはちょっと面倒な気がしますね。
もちろん、Beanの中でファイル読ませればいいじゃないか。
と云う考え方もできなくはないんですけどね。
CSSネスト
これは完全にメモです。
最近のCSSは便利で、いちいち.
やなんやで数珠つなぎにしなくても関連性をもたせられるようですね。
今回は、テーブルの一番外の要素table
で使うスタイルを指定したとき、その中のth
やtd
も、そのtable
に合わせてスタイルを自動的に決定したい場合をサンプルにします。
table.blue {
margin : 18px 0 18px 0;
border-collapse: collapse;
border : 1px solid #000000;
th {
background-color: #4472C4;
color : #FFFFFF;
padding : 0px 5px 0px 5px;
border-left : 1px solid #FFFFFF;
border-bottom : 3px double #000000;
}
/* 最初の要素のみ効くスタイル */
th:first-child {
border-left: none;
}
td {
border-left :1px solid #000000;
}
}
<table class="blue">
<thead>
<tr>
<th>番号</th>
<th>名前</th>
<th>年齢</th>
<th>誕生日</th>
</tr>
</thead>
<tbody>
<tr th:each="data : ${tdmm.dataList}">
<td th:text="${data.uNo}"></td>
<td th:text="${data.uName}"></td>
<td th:text="${data.age}"></td>
<td th:text="${data.bDate}"></td>
</tr>
</tbody>
</table>
最近だと、このように、スタイルシート上table
の中にネスト(入れ子)にしてその中の要素のスタイルを定義しておくと、大本のtable
にスタイルを指定すると中身も勝手に定義したスタイルを適用してくれます。
もちろん、CSSネストができる以前もできたのですが、定義が面倒くさくて見にくかったんですよね。
それがこんなにすっきりとして、便利になりましたね。
スッキリ度を伝えるために、以前の書き方も載せておきます。
table.blue {
margin: 18px 0 18px 0;
border-collapse: collapse;
border: 1px solid #000000;
}
table.blue th {
background-color: #4472C4;
color: #FFFFFF;
padding: 0px 5px 0px 5px;
border-left: 1px solid #FFFFFF;
border-bottom: 3px double #000000;
}
/* 最初の要素のみ効くスタイル */
table.blue th:first-child {
border-left: none;
}
table.blue td {
border-left: 1px solid #000000;
}
使うデータソース(DB)をアプリケーション中に動的に切り替える
最後にこちらのテーマです。
この記事を執筆した真の目的とも言えるこのテーマ。
これは本当に難しかった。
データソースを切り替えながら使う方法自体は紹介されている記事も色々あるのですが、紹介されているやり方がだと目的に合わなかったり、やり方が複雑すぎて環境に合わなかったり、最終的に答えを導き出すまでかなりの時間とエネルギーを使いました。
よく紹介されているのは次のように@Transactional
に使うデータソース(トランザクションマネージャー)を指定する方法です。
@Transactional(transactionManager = "顧客管理DB用")
public List<UserTable> submitTargetDB(String targetDB) {
// 顧客管理DBに向けた処理
}
@Transactional(transactionManager = "社内DB用")
public List<UserTable> submitTargetDB(String targetDB) {
// 社内DBに向けた処理
}
確かに、こうすればデータソースは切り替えられます。
この例のように、DBごとに役割が違う場合はこれでいいのです。
役割が違うということは、テーブル構成も違えば、そもそもDBごとに対する操作そのものが違います。
しかし、冒頭で触れたように、筆者が作ろうとしていたのはテストデータを投入するツールなので、そうではないんです。
UT用やIT、ST用など、まったく同じ構成のDBに、まったく同じことがしたいのです。
サンプルの場合はデータの投入先とするDBの選択によって、投入先のDBを決めていますが、例のやり方だと、まったく同じことをするのに@Transactional
の値が違うだけの処理をDBの数だけ定義しないといけないことになります。
そんなのは実装としてダサすぎる。
Spring Bootがそんなダサい実装しかできないなんてことはないはずだ。
と考えたところから、戦いが始まり、色々試しました。
トランザクションやデータソースの管理を自力で管理するようにMyBatisの動きを書いたり、Spring AOPでトランザクション制御や、カスタムアノテーションを実装してみたり、色々と・・・
しかしながら、うまくいかず、某AIに考え方を相談している間に一つのひらめきがあり、解答にたどり着くことができました。
まあ正直、自力でMyBatisを制御すればできたんですが、手間がかかりすぎるのでなしにしました。
たどり着いてみると答えはシンプルでした。
結論、アノテーションでデータソースを指定する方法でよかったのです。
@Transactional(transactionManager = "データソース")
public List<UserTable> submitTargetDB(String targetDB) {
// 処理
}
ミソになるのは、指定するデータソースを動的にする。
と云うことでした。
イメージが伝わりにくいと思います。
それだけ聞くと、「わかっとるわ。そのやり方がわからないんじゃい。」と思う方もいるかもしれません。
しかし、ここで動的にすると言っているのは、アノテーションに指定するデータソースに、動的データソースを指定する。
と云うことなのです。
実際のソースコードのイメージです。
@Transactional(transactionManager = "dynamicRoutingTransactionManager") // dynamicRoutingTransactionManagerは、場合によって接続先の違うデータソースに変化する。
public List<UserTable> submitTargetDB(String targetDB) {
// 処理
}
ここだけ見るとdynamicRoutingTransactionManager
は、ただの一つのデータソースですが、内部では、設定した条件に応じてデータベースの接続先や接続ユーザーを使い分けるよう実装した自作のデータソースです。
ここから、実装を紹介していきます。
データソースの定義
まずは、データソースを定義します。
定義先は、application.propertiesまたはymlです。
spring:
application:
name: TEST DATA MAKER
# データソースは複数定義する
datasource:
hikari:
auto-commit: false
UT1:
# TNS_ADMINも?の後に書けるらしいが変なエラーになったので1回封印
# jdbc-url: jdbc:oracle:thin:@ORCL?TNS_ADMIN=classpath:/tns
jdbc-url: jdbc:oracle:thin:@localhost:1521/ORCL
username: USER01
password: ORACLE
hikari:
auto-commit: false
IT1:
jdbc-url: jdbc:oracle:thin:@localhost:1521/ORCL
username: USER02
password: ORACLE
hikari:
auto-commit: false
thymeleaf:
cache: false
# ログレベルをデバッグに設定する
logging:
level:
org:
springflamework:
boot:
autoconfigure:
jdbc: DEBUG
jdbc:
datasource: DEBUG
root: DEBUG
# Springで管理しているBeanを確認できる設定
management:
endpoints:
web:
exposure:
include: beans, health, info
# ORACLEのVMがデフォルトの8080を使っているので変更
server:
port: 8083
ここでは、datasource
の要素に単体テスト用のUT1と、結合テスト用のIT1のデータソースを定義しています。
しかし、これはSpring標準のツリーではないので、この設定が読み込めるように、読み込みロジックを作ります。
また、同時に、各データソースが使うトランザクションマネージャー、各データソースのキーなども定義します。
これらの定義はどうしてもデータソースの数分必要になりますが、そこはあきらめるしかありません。
package jp.co.taro.config;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import jp.co.taro.datasource.RoutingDataSource;
/**
* <h1>[データソースの設定]</h1></br>
*<br>
* アプリケーションで使うデータソースとかトランザクションマネージャーとか色々を設定する。<br>
* 複数のデータソースを使うとき、application.properties、ymlに定義したデータソースはここの設定により有効なBeanとなる。
*/
@Configuration
public class DataSourceConfig {
/**
* <h2>[UT1用のデータソースBean]</h2></br>
*<br>
* UT1用の定義からデータソースを生成して返す。
* @param url 接続先URL
* @param username 接続ユーザー名
* @param password パスワード
* @return UT1用のデータソース
*/
@Bean
DataSource ut1DataSource(
@Value("${spring.datasource.UT1.jdbc-url}") String url, // ymlから読み込むキーは@Valueで指定できる
@Value("${spring.datasource.UT1.username}") String username,
@Value("${spring.datasource.UT1.password}") String password) {
// --------------------------------------------------------------------
// SpringではデフォルトでHikariCPというライブラリが使われるらしい。
// 特に独自の何かを使わないのであれば、
// 設定はHikariConfigをインスタンス化する。
// --------------------------------------------------------------------
HikariConfig config = new HikariConfig();
// Configに必要な情報を設定
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
// --------------------------------------------------------------------
// 自動COMMITをfalseにしておく。
// ymlに設定してあるが、なぜか効かない場合があるので、
// ここで明示的に設定する。
// 効いている場合は不要。
// --------------------------------------------------------------------
config.setAutoCommit(false);
// 整えた設定をセットしてデータソースを返却
return new HikariDataSource(config);
}
/**
* <h2>[UT1用のデータソース用のトランザクションマネージャー]</h2><br>
*<br>
* トランザクションの管理を司るトランザクションマネージャーなるものを返す。<br>
* これがないとおそらくトランザクションの管理がうまくいかない。
* @param ut1DataSource UT1用のデータソース
* @return UT1用のデータソースのトランザクションマネージャー
*/
@Bean
PlatformTransactionManager ut1TransactionManager(DataSource ut1DataSource) {
return new DataSourceTransactionManager(ut1DataSource);
}
/**
* <h2>[IT1用のデータソースBean]</h2></br>
*<br>
* IT1用の定義からデータソースを生成して返す。
* @param url 接続先URL
* @param username 接続ユーザー名
* @param password パスワード
* @return UT1用のデータソース
*/
@Bean
DataSource it1DataSource(
@Value("${spring.datasource.IT1.jdbc-url}") String url,
@Value("${spring.datasource.IT1.username}") String username,
@Value("${spring.datasource.IT1.password}") String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setAutoCommit(false);
return new HikariDataSource(config);
}
/**
* <h2>[IT1用のデータソース用のトランザクションマネージャー]</h2><br>
*<br>
* トランザクションの管理を司るトランザクションマネージャーなるものを返す。<br>
* これがないとおそらくトランザクションの管理がうまくいかない。
* @param it1DataSource UT1用のデータソース
* @return IT1用のデータソースのトランザクションマネージャー
*/
@Bean
PlatformTransactionManager it1TransactionManager(DataSource it1DataSource) {
return new DataSourceTransactionManager(it1DataSource);
}
/**
* <h2>[AbstractRoutingDataSourceのBean]</h2><br>
*<br>
* 使う予定のデータソースと、データソースを指定するる時のキーを決める。<br>
* キーとデータソースをセットしたMapは、RoutingDataSourceで選択できる要素になる。
* @param ut1DataSource UT用のデータソース
* @param it1DataSource IT用のデータソース
* @return ロジックにより決定したデータソース
*/
@Bean
AbstractRoutingDataSource routingDataSource(
@Qualifier("ut1DataSource") DataSource ut1DataSource,
@Qualifier("it1DataSource") DataSource it1DataSource) {
// 定義したAbstractRoutingDataSourceの実装を返すデータソースとする
RoutingDataSource routingDataSource = new RoutingDataSource();
// --------------------------------------------------------------------
// 使う予定のデータソースを探すときのキーと、
// 実際のデータソースをMapする。
// RoutingDataSourceでデータソースを選ぶときのキーは、
// ここで設定したものになる。
// --------------------------------------------------------------------
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("UT1", ut1DataSource);
targetDataSources.put("IT1", it1DataSource);
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
/**
* <h2>[MyBatisで使うセッションを作るクラスのBean]</h2><br>
*<br>
* MyBatisで使うセッションを作る機能を持つクラスのBeanを返す。<br>
* セッションを作るときに使うデータソースと、Mapperのパスを設定する。
* @param routingDataSource AbstractRoutingDataSourceのBean
* @return セッションを作るクラスのBean
* @throws Exception 想定しないエラー発生時
*/
@Bean
SqlSessionFactory sqlSessionFactory(@Qualifier("routingDataSource") DataSource routingDataSource) throws Exception {
// --------------------------------------------------------------------
// セッションを作るBeanにデータソース、Mapperのパスを設定する。
// --------------------------------------------------------------------
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// ここで設定するデータをーすを"routingDataSource"すなわち動的データソースにするのが肝
factoryBean.setDataSource(routingDataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/jp/co/taro/mapper/*.xml"));
return factoryBean.getObject();
}
/**
* <h2>[プラットフォームのトランザクションを司るトランザクションマネージャーのBean]</h2><br>
*<br>
* Springが使うトランザクションマネージャーを返す。<br>
* Springには、RoutingDataSourceにより、動的にデータソースを切り替えさせたい。<br>
* そのために、トランザクションマネージャーには、routingDataSourceを設定して返す。
* @param routingDataSource
* @return 動的データソース
*/
@Bean
PlatformTransactionManager dynamicRoutingTransactionManager(
@Qualifier("routingDataSource") DataSource routingDataSource) {
// 動的にデータソースを決定する仕組みをセットする
return new DataSourceTransactionManager(routingDataSource);
}
}
余談ですが、Springbootのいつからかのバージョンで、Beanを定義するときpublic
が必要なくなったようです。
データソース選択ロジックの定義
続いて、どういうロジックでデータソースを選択するか?と云うロジックを定義します。
今回のサンプルでは、画面で選択したDBをstaticオブジェクトに持っておいて、その選択によって使うデータソースを決定します。
データソースの選択ロジックは、AbstractRoutingDataSource
を拡張して定義します。
package jp.co.taro.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import jp.co.taro.share.TargetDB;
/**
* <h1>[データソース決定]</h1><br>
*<br>
* 動的にデータソースを切り替えるための肝。
* 動的にデータソースを切り替えるための判断ロジックを持つ。
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
/** ロガー */
private static final Logger logger = LoggerFactory.getLogger(RoutingDataSource.class);
/**
* {@inheritDoc}
*/
@Override
protected Object determineCurrentLookupKey() {
logger.debug("データソースキーの選択開始");
// --------------------------------------------------------------------
// どのデータソースを使うか判断するロジックを実装する。
// --------------------------------------------------------------------
String targetDB = TargetDB.getInstance().getTargetDB();
// --------------------------------------------------------------------
// ここでは、入力した内容から使うデータソースを決定する。
// イメージを伝えやすくするためにワザとダサく分岐させる。
// --------------------------------------------------------------------
String key = null;
if("UT1".equals(targetDB)) {
// 返すキーはDataSourceConfigで定義したAbstractRoutingDataSourceのBeanで設定したキーに合わせる
key = "UT1";
} else {
key = "IT1";
}
logger.debug("データソースキーの選択終了");
return key;
}
}
DatabaseContextHolder
の定義
筆者もよくわかっていないのですが、データソースの情報を保持しておく用のものなのか、DatabaseContextHolder
と云うものを定義します。
package jp.co.taro.datasource;
/**
* <h1>[データソースのContextを持つ]</h1></br>
*<br>
* 仕組みはよくわからないが、データソースのContextなるものを持つらしい。<br>
* 複数のデータソースを扱うには、このクラスの定義が必須の模様。
*/
public class DatabaseContextHolder {
/** コンテキストホルダー */
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* <h2>[データソースのタイプ]</h2></br>
* @param dbType データソースのタイプ("primary"とか"secondary"とかを持つらしい)
*/
public static void setCurrentDatabase(String dbType) {
contextHolder.set(dbType);
}
/**
* <h2>[現在のデータソース取得]</h2><br>
* @return 現在使っているデータソース
*/
public static String setCurrentDatabase() {
return contextHolder.get();
}
/**
* <h2>[初期化]</h2><br>
*<br>
* データソースを初期化する。
* これ重要。
*/
public static void clear() {
contextHolder.remove();
}
}
MyBatisの設定
最後に、MyBatisの設定をします。
普通に使う分にはこんなクラスはなくても使えるのですが、なぜかこの仕組みを使おうとすると、MapperScanの設定だけはないと起動エラーになります。
まあ、クラスがあればその他に、キャッシュをオフにしたり、色々できるので、あって損になるものではありません。
package jp.co.taro.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
/**
* <h1>[MyBatisの設定]</h1></br>
*<br>
* 何の役に立つかわかっていないが、とりあえず必要らしい。
*/
@Configuration
@MapperScan("jp.co.taro.mapper")
public class MyBatisConfig {
// ------------------------------------------------------------------------
// MapperScanを書くだけで後は何もしない。
// 必要に応じて中身を追加する。
// ------------------------------------------------------------------------
}
ここまでで、データソースを動的に切り替える仕組みの実装は完了です。
あとは、MyBatisでSQLを実装したりは必要ですが、それらは普通に使うのと変わりないので、当記事では割愛します。
動的データソースを使う
それでは、実際に動的データソースを使います。
先ほどソースコードを抜粋して触れていますが、使う方はコントローラーやサービスのDB操作を行う処理のアノテーションに、dynamicRoutingTransactionManager
を指定するだけです。
dynamicRoutingTransactionManager
は、先ほどDataSourceConfig
で、プラットフォームが使うトランザクションマネージャーとしてBeanを定義しました。
package jp.co.taro.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jp.co.taro.entity.UserTable;
import jp.co.taro.mapper.UserTableMapper;
/**
* <h1>[テストデータ作成Service]</h1><br>
*<br>
* SpringBootでは、業務処理はServiceと名前の付くクラスに実装し、
* コントローラーから切り離すのが標準的な設計らしい。
*/
@Service
public class TestDataMakerService {
/** テストデータのMapper */
@Autowired
private UserTableMapper userTableMapper;
/**
* <h2>[対象DBの決定]</h2></br>
*<br>
* 対象のDBから、今入っているデータを取得して返す。
* @param targetDB 対象DB
* @return 今入っているデータのリスト
*/
@Transactional(transactionManager = "dynamicRoutingTransactionManager")
public List<UserTable> submitTargetDB(String targetDB) {
return userTableMapper.select();
}
}
サンプルではサービスから使っていますが、先にも述べたとおり、MyBatisや他の使い方は、データソースを固定で使っている時と変わらないので、コントローラーからでも、それらとは別に切り離したユーティリティー的なところからでも、自由に使えます。
ここまでで、使うデータソース(DB)をアプリケーション中に動的に切り替える方法の紹介は終わりです。
おわりに
結構な量割愛したのですが、今回も結構な執筆量になってしまいました。
量が多くて目的の部分が探しにくかったと云う方、すみません。
しかしながら、当記事の内容が、これからSpring Bootを使って開発をしていく皆さまの一助になれましたなら、それ以上にうれしいことはありません。
毎度のことですが、当記事も筆者のメモを兼ねていますので、今後メモして起きたことが増えときには当記事を更新していきます。
新しい記事を作ってもいいのですが、ノウハウが散らばるのも面倒になるので、今後、Spring Boot関係のちょっとしたメモは当記事を更新していく方針としようと思います。
革命的な更新はないかもしれませんが、更新の通知が届いた方は、メモが増えたのだとお考えください。