Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、同一トランザクション内で複数DBに接続するアプリケーションの場合は、複数DBのトランザクション管理を行うための仕組みが別途必要になる。
その対応案の1つとして、ChainedTransactionManagerを利用し、接続先DBをまとめて管理したChainedTransactionManagerクラスのインスタンスをあらかじめ定義しておき、@Transactionalアノテーション内で指定するtransactionManager属性にChainedTransactionManagerを指定するという方法がある。
今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、1つのトランザクションで複数のDBを更新する処理を記載し、ChainedTransactionManagerを利用した@Transactionalアノテーションの挙動を調べてみたので、共有する。
前提条件
下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加/変更したプログラムである。
build.gradleの内容は以下の通りで、ChainedTransactionManagerを利用するために、Spring Data Commonsのライブラリを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | plugins { id 'org.springframework.boot' version '2.1.7.RELEASE' id 'java' } apply plugin: 'io.spring.dependency-management' group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' compileOnly 'org.projectlombok:lombok:1.18.10' annotationProcessor 'org.projectlombok:lombok:1.18.10' //Oracleに接続するための設定 compile files('lib/ojdbc6.jar') //SQL Serverに接続するための設定 compile group: 'com.microsoft.sqlserver', name: 'mssql-jdbc', version: '8.4.1.jre11' //MyBatisを利用するための設定 implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1' //ChainedTransactionManagerを利用するために追加 implementation group: 'org.springframework.data', name: 'spring-data-commons', version: '2.4.7' } |
また、ChainedTransactionalアノテーションを定義しているクラスは以下の通りで、OracleとSQL Serverのトランザクションマネージャを1つにまとめて定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package com.example.demo.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.transaction.ChainedTransactionManager; import org.springframework.transaction.PlatformTransactionManager; @Configuration public class DemoDataSourceConfig { /** * 複数トランザクションマネージャを1つにまとめて定義する * @param txManagerOra Oracleのトランザクションマネージャ * @param txManagerSs SQL Serverのトランザクションマネージャ * @return 複数トランザクションマネージャを1つにまとめて定義したChainedTransactionManager */ @Bean(name = "txManagerChained") public ChainedTransactionManager transactionManager( @Qualifier("txManagerOra") PlatformTransactionManager txManagerOra, @Qualifier("txManagerSs") PlatformTransactionManager txManagerSs) { return new ChainedTransactionManager(txManagerOra, txManagerSs); } } |
なお、OracleのトランザクションマネージャはDemoOraDataSourceConfigクラスで、SQL ServerのトランザクションマネージャはDemoSsDataSourceConfigクラスで定義している。
さらに、サービスクラスの内容は以下の通りで、OracleとSQL Serverのユーザーデータテーブル(user_data)を1つのトランザクションで更新する処理を定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | package com.example.demo; import com.example.demo.mapper.ora.UserDataMapperOra; import com.example.demo.mapper.ss.UserDataMapperSs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DemoService { /** 2回目の更新後氏名を定義 */ /** * USER_NAME_OK:13桁39バイトで更新OK、USER_NAME_NG:41桁で更新NG */ private static String USER_NAME_OK = "1234567890123"; private static String USER_NAME_NG = "12345678901234567890123456789012345678901"; /** * Oracleのユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapperOra userDataMapperOra; /** * SQL Serverのユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapperSs userDataMapperSs; /** * ChainedTransactionManagerを利用して、OracleとSQL Serverの * ユーザーデータテーブル(user_data)を更新するトランザクション */ @Transactional(transactionManager = "txManagerChained") public void transUserData() { UserData userDataOra = userDataMapperOra.findById(Long.valueOf(1)); UserData userDataSs = userDataMapperSs.findById(Long.valueOf(1)); // ユーザーデータが取得できなければ処理を終了する if (userDataOra == null || userDataSs == null) { System.out.println("OracleまたはSQL Serverのいずれかで、" + "id=1のユーザーデータは見つかりませんでした。"); return; } // 更新前データを表示 System.out.println("ユーザーデータ(Oracle更新前) : " + userDataOra.toString()); System.out.println("ユーザーデータ(SQL Server更新前) : " + userDataSs.toString()); // 氏名を更新する userDataOra.setName("テスト プリン1 更新後"); userDataSs.setName("テスト プリン1 更新後"); userDataMapperOra.update(userDataOra); userDataMapperSs.update(userDataSs); // 更新後データを表示 userDataOra = userDataMapperOra.findById(Long.valueOf(1)); userDataSs = userDataMapperSs.findById(Long.valueOf(1)); System.out.println("ユーザーデータ(Oracle1回目更新後) : " + userDataOra.toString()); System.out.println("ユーザーデータ(SQL Server1回目更新後) : " + userDataSs.toString()); // 氏名を再度更新する userDataOra.setName(USER_NAME_OK); userDataSs.setName(USER_NAME_OK); userDataMapperOra.update(userDataOra); userDataMapperSs.update(userDataSs); // 更新後データを表示 userDataOra = userDataMapperOra.findById(Long.valueOf(1)); userDataSs = userDataMapperSs.findById(Long.valueOf(1)); System.out.println("ユーザーデータ(Oracle2回目更新後) : " + userDataOra.toString()); System.out.println("ユーザーデータ(SQL Server2回目更新後) : " + userDataSs.toString()); } } |
また、Spring Bootのメインクラスの内容は以下の通りで、サービスクラスのtransUserDataメソッドを呼び出している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication implements CommandLineRunner { /** * ユーザーデータテーブル(user_data)を更新する * トランザクションを含むサービス */ @Autowired private DemoService demoService; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) { try { // ChainedTransactionManagerを利用して、 // OracleとSQL Serverのユーザーデータテーブル(user_data)を // 更新するトランザクションを呼び出す // DB更新に失敗するとExceptionがスローされる demoService.transUserData(); } catch (Exception ex) { System.err.println(ex); } } } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-chained-transaction-manager/demo
サンプルプログラムの実行結果
サンプルプログラムの実行結果は以下の通りで、同一トランザクション内でOracleとSQL Serverの両方のDB更新が成功するとコミット、どちらか1つでも失敗するとロールバックされることが確認できる。
1) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、OracleもSQL Serverも更新成功する値に設定する。
2) 1)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
3) 1)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、コミットされ氏名が更新されることが確認できる。
<実行前(Oracle)>
1 | select * from user_data where id = 1 |
<実行前(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
<実行後(Oracle)>
1 | select * from user_data where id = 1 |
<実行後(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
4) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、Oracleは更新失敗し、SQL Serverは更新成功する値に設定する。
5) 4)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
6) 4)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、OracleもSQL Serverもロールバックされ氏名が更新されないことが確認できる。
<実行前(Oracle)>
1 | select * from user_data where id = 1 |
<実行前(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
<実行後(Oracle)>
1 | select * from user_data where id = 1 |
<実行後(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
7) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、SQL Serverは更新失敗し、Oracleは更新成功する値に設定する。
8) 7)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
9) 7)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、OracleもSQL Serverもロールバックされ氏名が更新されないことが確認できる。
<実行前(Oracle)>
1 | select * from user_data where id = 1 |
<実行前(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
<実行後(Oracle)>
1 | select * from user_data where id = 1 |
<実行後(SQL Server)>
1 | select * from dbo.user_data where id = 1 |
要点まとめ
- 同一トランザクション内で複数DBに接続するアプリケーションの場合は、接続先DBをまとめて管理したChainedTransactionManagerクラスのインスタンスをあらかじめ定義しておき、@Transactionalアノテーション内で指定するtransactionManager属性にChainedTransactionManagerを指定することで、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができる。