Spring Boot DB連携

1つのトランザクションで複数のDBを更新するアプリケーションでChainedTransactionManagerを利用してみた

Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、同一トランザクション内で複数DBに接続するアプリケーションの場合は、複数DBのトランザクション管理を行うための仕組みが別途必要になる。

その対応案の1つとして、ChainedTransactionManagerを利用し、接続先DBをまとめて管理したChainedTransactionManagerクラスのインスタンスをあらかじめ定義しておき、@Transactionalアノテーション内で指定するtransactionManager属性にChainedTransactionManagerを指定するという方法がある。

今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、1つのトランザクションで複数のDBを更新する処理を記載し、ChainedTransactionManagerを利用した@Transactionalアノテーションの挙動を調べてみたので、共有する。

前提条件

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

複数DBに接続するアプリケーションでSpring Bootの@Transactionalアノテーションの挙動を調べてみたSpring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でD...

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

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

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

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つにまとめて定義している。

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

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メソッドを呼び出している。

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



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

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

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

1) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、OracleもSQL Serverも更新成功する値に設定する。
サンプルプログラムの実行結果_1

2) 1)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
サンプルプログラムの実行結果_2

3) 1)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、コミットされ氏名が更新されることが確認できる。

<実行前(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_3_1

<実行前(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_3_2

<実行後(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_3_3

<実行後(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_3_4



ウズウズカレッジJavaコースはわかりやすい動画教材と充実した就業サポートで優良企業を目指せるプログラミングスクールだったJavaは、世界中で広く使われていて、現在の需要が高く将来性もある開発言語になります。 https://www.acrovision....

4) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、Oracleは更新失敗し、SQL Serverは更新成功する値に設定する。
サンプルプログラムの実行結果_4

5) 4)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
サンプルプログラムの実行結果_5

6) 4)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、OracleもSQL Serverもロールバックされ氏名が更新されないことが確認できる。

<実行前(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_6_1

<実行前(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_6_2

<実行後(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_6_3

<実行後(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_6_4



「DesignEvo」は多くのテンプレートからロゴを簡単に作成できるツールだった多くのテンプレートが用意されていてロゴを簡単に作成できるツールの一つに、「DesignEvo」があります。今回は、「DesignEvo」...

7) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、SQL Serverは更新失敗し、Oracleは更新成功する値に設定する。
サンプルプログラムの実行結果_7

8) 7)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
サンプルプログラムの実行結果_8

9) 7)の状態で、実行前後でOracleとSQL ServerのUSER_DATAテーブルの値を確認した結果は以下の通りで、OracleもSQL Serverもロールバックされ氏名が更新されないことが確認できる。

<実行前(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_9_1

<実行前(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_9_2

<実行後(Oracle)>

select * from user_data where id = 1
サンプルプログラムの実行結果_9_3

<実行後(SQL Server)>

select * from dbo.user_data where id = 1
サンプルプログラムの実行結果_9_4

要点まとめ

  • 同一トランザクション内で複数DBに接続するアプリケーションの場合は、接続先DBをまとめて管理したChainedTransactionManagerクラスのインスタンスをあらかじめ定義しておき、@Transactionalアノテーション内で指定するtransactionManager属性にChainedTransactionManagerを指定することで、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができる。