複数のデータベース間で一連のデータ操作を行うことを「分散トランザクション」といい、分散トランザクションにおいてデータの整合性を管理できるオープンソースのJavaライブラリに「Atomikos」がある。
トランザクション・分散トランザクションの概念については、以下のサイトを参照のこと。
https://qiita.com/yShig/items/0168e651d6f3ef105f35
今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、1つのトランザクションで複数のDB(OracleとSQL Server)を更新する処理を記載し、その際にAtomikosを利用するサンプルプログラムを作成してみたので、共有する。
前提条件
下記記事の実装が完了していること。
やってみたこと
SQL Serverで分散トランザクションを利用するための設定
SQL Serverで分散トランザクションを利用できるようにするには、いくつかの設定変更が必要である。その手順は、以下の通り。
1) スタートメニューで「コンポーネント」を入力するなどして、コンポーネントサービスを開く。
2) 「Distributed Transaction Coordinator」内、「ローカル DTC」の「プロパティ」を選択する。
3) 「セキュリティ」タブで「XA トランザクションを有効にする」にチェックを入れ、「OK」ボタンを押下する。
4) 下記ダイアログが表示されるため、「はい」ボタンを押下する。
5) MSDTCサービスを再開した旨のダイアログが表示されるため、「OK」ボタンを押下する。
6) SQL Server 構成マネージャーを起動するため、「C:\Windows\SysWOW64」フォルダ内の「SQLServerManager15.msc」を選択しダブルクリックする。
なお、SQL Server 構成マネージャーについては、以下のサイトを参照のこと。
https://docs.microsoft.com/ja-jp/sql/database-engine/configure-windows/start-stop-pause-resume-restart-sql-server-services?view=sql-server-ver15#sql-server-configuration-manager
7) SQL Serverのサービスから「SQL Server(MSSQLSERVER)」を選択し右クリックし、「再起動」メニューを押下する。これで、SQL Serverが再起動される。
8) 分散トランザクションを利用できる権限を付与するため、SSMS(SQL Server Management Studio)でSQL Serverに接続後、ストアドプロシージャ「sys.sp_sqljdbc_xa_install」を実行する。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加/変更したプログラムである。
build.gradleの内容は以下の通りで、Atomikosを利用するためのライブラリを追加している。
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' //Atomikosを利用するための設定 implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos' } |
また、OracleとSQL Serverへ接続するための定義クラスは以下の通りで、データソースの生成にAtomikosを利用できるようにしている。
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 78 79 80 81 82 83 84 85 86 87 88 89 | package com.example.demo.config; import oracle.jdbc.xa.client.OracleXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.sql.SQLException; @Configuration @MapperScan(basePackages = {"com.example.demo.mapper.ora"} , sqlSessionFactoryRef = "sqlSessionFactoryOra") public class DemoOraDataSourceConfig { /** * Oracleのデータソースプロパティを生成する * @return Oracleのデータソースプロパティ */ @Bean(name = {"datasourceOraProperties"}) @Primary @ConfigurationProperties(prefix = "spring.datasource") public DataSourceProperties datasourceOraProperties() { return new DataSourceProperties(); } /** * Oracleのデータソースを生成する * @param properties Oracleのデータソースプロパティ * @return Oracleのデータソース * @throws SQLException SQL例外 */ @Bean(name = {"dataSourceOra"}) @Primary public DataSource datasourceOra( @Qualifier("datasourceOraProperties") DataSourceProperties properties) throws SQLException { // OracleXAデータソースオブジェクトを作成 OracleXADataSource xaDataSource = new OracleXADataSource(); // URL・ユーザー名・パスワードを引数の定義ファイルから取得し設定 xaDataSource.setURL(properties.getUrl()); xaDataSource.setUser(properties.getUsername()); xaDataSource.setPassword(properties.getPassword()); // AtomikosデータソースBeanオブジェクトを生成 AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); // 一意なリソース名・OracleXAデータソースオブジェクト・コネクションプールサイズを設定し返却 atomikosDataSourceBean.setUniqueResourceName("atomikosDataSourceOra"); atomikosDataSourceBean.setXaDataSource(xaDataSource); atomikosDataSourceBean.setPoolSize(5); return atomikosDataSourceBean; } /** * Oracleのトランザクションマネージャを生成する * @param dataSourceOra Oracleのデータソース * @return Oracleのトランザクションマネージャ */ @Bean(name = {"txManagerOra"}) @Primary public PlatformTransactionManager txManagerOra( @Qualifier("dataSourceOra") DataSource dataSourceOra) { return new DataSourceTransactionManager(dataSourceOra); } /** * OracleのSQLセッションファクトリを生成する * @param dataSourceOra Oracleのデータソース * @return OracleのSQLセッションファクトリ * @throws Exception 任意例外 */ @Bean(name = {"sqlSessionFactoryOra"}) @Primary public SqlSessionFactory sqlSessionFactory( @Qualifier("dataSourceOra") DataSource dataSourceOra) throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSourceOra); return sqlSessionFactory.getObject(); } } |
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 78 79 80 81 | package com.example.demo.config; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import com.microsoft.sqlserver.jdbc.SQLServerXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan(basePackages = {"com.example.demo.mapper.ss"} , sqlSessionFactoryRef = "sqlSessionFactorySs") public class DemoSsDataSourceConfig { /** * SQL Serverのデータソースプロパティを生成する * @return SQL Serverのデータソースプロパティ */ @Bean(name = {"datasourceSsProperties"}) @ConfigurationProperties(prefix = "spring.datasourcess") public DataSourceProperties datasourceSsProperties() { return new DataSourceProperties(); } /** * SQL Serverのデータソースを生成する * @param properties SQL Serverのデータソースプロパティ * @return SQL Serverのデータソース */ @Bean(name = {"dataSourceSs"}) public DataSource datasourceSs( @Qualifier("datasourceSsProperties") DataSourceProperties properties) { // SQLServerXAデータソースオブジェクトを作成 SQLServerXADataSource xaDataSource = new SQLServerXADataSource(); // URL・ユーザー名・パスワードを引数の定義ファイルから取得し設定 xaDataSource.setURL(properties.getUrl()); xaDataSource.setUser(properties.getUsername()); xaDataSource.setPassword(properties.getPassword()); // AtomikosデータソースBeanオブジェクトを生成 AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); // 一意なリソース名・SQLServerXAデータソースオブジェクト・コネクションプールサイズを設定し返却 atomikosDataSourceBean.setUniqueResourceName("atomikosDataSourceSs"); atomikosDataSourceBean.setXaDataSource(xaDataSource); atomikosDataSourceBean.setPoolSize(5); return atomikosDataSourceBean; } /** * SQL Serverのトランザクションマネージャを生成する * @param dataSourceSs SQL Serverのデータソース * @return SQL Serverのトランザクションマネージャ */ @Bean(name = {"txManagerSs"}) public PlatformTransactionManager txManagerSs( @Qualifier("dataSourceSs") DataSource dataSourceSs) { return new DataSourceTransactionManager(dataSourceSs); } /** * SQL ServerのSQLセッションファクトリを生成する * @param dataSourceSs SQL Serverのデータソース * @return SQL ServerのSQLセッションファクトリを生成する * @throws Exception 任意例外 */ @Bean(name = {"sqlSessionFactorySs"}) public SqlSessionFactory sqlSessionFactory( @Qualifier("dataSourceSs") DataSource dataSourceSs) throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSourceSs); return sqlSessionFactory.getObject(); } } |
さらに、Atomikosで利用する、トランザクション管理を行うJtaTransactionManagerを生成しているクラスの定義は、以下の通り。
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 | package com.example.demo.config; import com.atomikos.icatch.jta.UserTransactionImp; import com.atomikos.icatch.jta.UserTransactionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.transaction.jta.JtaTransactionManager; import javax.transaction.UserTransaction; @Configuration public class DemoDataSourceConfig { /** * トランザクション管理を行うJtaTransactionManagerを生成 * @return JtaTransactionManagerオブジェクト */ @Bean @Primary public JtaTransactionManager regTransactionManager() { UserTransactionManager userTransactionManager = new UserTransactionManager(); UserTransaction userTransaction = new UserTransactionImp(); return new JtaTransactionManager(userTransaction, userTransactionManager); } } |
また、サービスクラスの内容は以下の通りで、Atomikosを利用して、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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 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.jta.JtaTransactionManager; import javax.transaction.UserTransaction; @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; /** * Atomikosを利用した場合のトランザクション管理 */ @Autowired private JtaTransactionManager jtaTransactionManager; /** * Atomikosを利用して、OracleとSQL Serverのユーザーデータテーブル(user_data)を * 更新するトランザクション * @throws Exception 任意例外 */ public void transUserData() throws Exception { //トランザクションを開始する UserTransaction userTransaction = jtaTransactionManager.getUserTransaction(); userTransaction.begin(); try { // id=1のユーザーデータを取得する 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()); //トランザクションをコミットする userTransaction.commit(); } catch (Exception ex) { System.err.println(ex); //トランザクションをロールバックする userTransaction.rollback(); } } } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-oracle-sqlserver-atomikos/demo
サンプルプログラムの実行結果
サンプルプログラムの実行結果は、以下の記事に記載した内容と同じで、同一トランザクション内でOracleとSQL Serverの両方のDB更新が成功するとコミット、どちらか1つでも失敗するとロールバックされることが確認できる。
要点まとめ
- 複数のデータベース間で一連のデータ操作を行うことを「分散トランザクション」といい、分散トランザクションにおいてデータの整合性を管理できるオープンソースのJavaライブラリに「Atomikos」がある。
- SQL Serverの場合、XA トランザクションを有効にする設定を行ってSQL Serverを再起動した後で、ストアドプロシージャ「sys.sp_sqljdbc_xa_install」を実行することで、分散トランザクションを利用できる。