Spring Boot DB連携

1つのトランザクションで複数のDB(OracleとSQL Server)を更新するアプリケーションでAtomikosを利用してみた

複数のデータベース間で一連のデータ操作を行うことを「分散トランザクション」といい、分散トランザクションにおいてデータの整合性を管理できるオープンソースのJavaライブラリに「Atomikos」がある。

トランザクション・分散トランザクションの概念については、以下のサイトを参照のこと。
https://qiita.com/yShig/items/0168e651d6f3ef105f35

今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、1つのトランザクションで複数のDB(OracleとSQL Server)を更新する処理を記載し、その際にAtomikosを利用するサンプルプログラムを作成してみたので、共有する。

前提条件

下記記事の実装が完了していること。

1つのトランザクションで複数のDBを更新するアプリケーションでChainedTransactionManagerを利用してみたSpring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でD...

やってみたこと

  1. SQL Serverで分散トランザクションを利用するための設定
  2. サンプルプログラムの作成
  3. サンプルプログラムの実行結果

SQL Serverで分散トランザクションを利用するための設定

SQL Serverで分散トランザクションを利用できるようにするには、いくつかの設定変更が必要である。その手順は、以下の通り。

1) スタートメニューで「コンポーネント」を入力するなどして、コンポーネントサービスを開く。
SQLServerの設定_1

2) 「Distributed Transaction Coordinator」内、「ローカル DTC」の「プロパティ」を選択する。
SQLServerの設定_2

3) 「セキュリティ」タブで「XA トランザクションを有効にする」にチェックを入れ、「OK」ボタンを押下する。
SQLServerの設定_3

4) 下記ダイアログが表示されるため、「はい」ボタンを押下する。
SQLServerの設定_4

5) MSDTCサービスを再開した旨のダイアログが表示されるため、「OK」ボタンを押下する。
SQLServerの設定_5

6) SQL Server 構成マネージャーを起動するため、「C:\Windows\SysWOW64」フォルダ内の「SQLServerManager15.msc」を選択しダブルクリックする。
SQLServerの設定_6

なお、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が再起動される。
SQLServerの設定_7_1

SQLServerの設定_7_2 SQLServerの設定_7_3 SQLServerの設定_7_4

8) 分散トランザクションを利用できる権限を付与するため、SSMS(SQL Server Management Studio)でSQL Serverに接続後、ストアドプロシージャ「sys.sp_sqljdbc_xa_install」を実行する。
SQLServerの設定_8



Code VillageはJavaScriptを中心としたサポート体制が充実したプログラミングスクールだったJavaScriptや、JavaScriptのフレームワーク「React」「Vue」を中心にオンラインで学習できるプログラミングスクール...

サンプルプログラムの作成

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、前提条件のプログラムから追加/変更したプログラムである。

build.gradleの内容は以下の通りで、Atomikosを利用するためのライブラリを追加している。

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を利用できるようにしている。

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();
    }
}
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を生成しているクラスの定義は、以下の通り。

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つのトランザクションで更新する処理を定義している。

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



「AOMEI Backupper」は様々な形でバックアップ取得や同期処理が行える便利ツールだったパソコン内のデータを、ファイル/パーティション/ディスク等の様々な単位でバックアップしたり、バックアップ時のスケジュール設定やリアルタイ...

サンプルプログラムの実行結果

サンプルプログラムの実行結果は、以下の記事に記載した内容と同じで、同一トランザクション内でOracleとSQL Serverの両方のDB更新が成功するとコミット、どちらか1つでも失敗するとロールバックされることが確認できる。

1つのトランザクションで複数のDBを更新するアプリケーションでChainedTransactionManagerを利用してみたSpring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でD...

要点まとめ

  • 複数のデータベース間で一連のデータ操作を行うことを「分散トランザクション」といい、分散トランザクションにおいてデータの整合性を管理できるオープンソースのJavaライブラリに「Atomikos」がある。
  • SQL Serverの場合、XA トランザクションを有効にする設定を行ってSQL Serverを再起動した後で、ストアドプロシージャ「sys.sp_sqljdbc_xa_install」を実行することで、分散トランザクションを利用できる。