Spring Boot DB連携

複数DBに接続するアプリケーションでSpring Bootの@Transactionalアノテーションの挙動を調べてみた

Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、複数DBに接続するアプリケーションの場合は、@Transactionalアノテーション内でtransactionManager属性を指定すればよい。

今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、OracleやSQL Serverに接続し@Transactionalアノテーションの挙動を調べてみたので、共有する。

前提条件

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

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

また、下記記事の前提条件を満たしていること。

Spring BootでSQL Serverに接続しMyBatisを利用してみた今回は、Spring Bootアプリケーションで接続するデータベースをSQL Serverに変更してみたので、そのサンプルプログラムを共...

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

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

build.gradleの内容は以下の通りで、SQL Serverに接続するためのJDBCライブラリを追加している。

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'
}

application.propertiesの内容は以下の通りで、SQL Serverに接続するための接続設定を追加している。

server.port = 8084
# DB接続先(Oracle)
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=USER01
spring.datasource.password=USER01
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
# DB接続先(SQLServer)
spring.datasourcess.url=jdbc:sqlserver://localhost:1433;databaseName=master
spring.datasourcess.username=USER01
spring.datasourcess.password=USER01
spring.datasourcess.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver



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

また、1つのSpring Bootアプリケーションから、複数DBに接続できるようにするには、Configクラスを作成する必要がある。

OracleのConfigクラスの内容は以下の通りで、DB接続に必要なデータソースプロパティ、データソース、トランザクションマネージャ、セッションファクトリを生成している。

package com.example.demo.config;

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.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@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のデータソース
     */
    @Bean(name = {"dataSourceOra"})
    @Primary
    public DataSource datasourceOra(
            @Qualifier("datasourceOraProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    /**
     * 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();
    }
}

データソースプロパティは、application.propertiesに指定した「spring.datasource」から取得するようにしている。また、1つのDB接続定義には@Primaryアノテーションを付与する必要があるため、OracleのConfigクラスの各メソッドに@Primaryアノテーションを付与している。

SQL ServerのConfigクラスの内容は以下の通りで、@Primaryアノテーションを付与しない状態で、Oracleの場合と同様に、DB接続に必要なリソースを生成している。

package com.example.demo.config;

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) {
        return properties.initializeDataSourceBuilder().build();
    }

    /**
     * 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();
    }
}



「AOMEI Partition Assistant Standard(無料)版」は便利なパーティション管理ツールだったハードディスクの記憶領域を論理的に分割し、分割された個々の領域のことを、パーティションといいます。 例えば、以下の図の場合、C/D...

各DBのMapperインタフェースの内容は以下の通りで、Configクラスの@MapperScanアノテーション内のbasePackagesで指定したパッケージ内に、それぞれ作成している。

package com.example.demo.mapper.ora;

import com.example.demo.UserData;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserDataMapperOra {

    /**
     * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
     * @param id ID
     * @return ユーザーデータテーブル(user_data)の指定したIDのデータ
     */
    UserData findById(Long id);

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを更新する
     * @param userData ユーザーデータテーブル(user_data)の更新データ
     */
    void update(UserData userData);
}
package com.example.demo.mapper.ss;

import com.example.demo.UserData;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserDataMapperSs {

    /**
     * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
     * @param id ID
     * @return ユーザーデータテーブル(user_data)の指定したIDのデータ
     */
    UserData findById(Long id);

    /**
     * 指定したユーザーデータテーブル(user_data)のデータを更新する
     * @param userData ユーザーデータテーブル(user_data)の更新データ
     */
    void update(UserData userData);
}

また、Mapperインタフェースから呼ばれるXMLファイルの内容は以下の通りで、前提条件の記事と同じ内容のSQLを記載している。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ora.UserDataMapperOra">
    <resultMap id="userDataResultMap" type="com.example.demo.UserData" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="birthY" property="birthY" jdbcType="VARCHAR" />
        <result column="birthM" property="birthM" jdbcType="VARCHAR" />
        <result column="birthD" property="birthD" jdbcType="VARCHAR" />
        <result column="sex" property="sex" jdbcType="VARCHAR" />
        <result column="memo" property="memo" jdbcType="VARCHAR" />
        <result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
    </resultMap>
    <select id="findById" parameterType="java.lang.Long" resultMap="userDataResultMap">
        SELECT
             id
           , name
           , birth_year as birthY
           , birth_month as birthM
           , birth_day as birthD
           , sex
           , memo
           , CASE sex
             WHEN '1' THEN '男'
             WHEN '2' THEN '女'
             ELSE ''
             END AS sex_value
        FROM USER_DATA
        WHERE id = #{id}
    </select>
    <update id="update" parameterType="com.example.demo.UserData">
        UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
           , birth_month = #{birthM}, birth_day = #{birthD}
           , sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
        WHERE id = #{id}
    </update>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ss.UserDataMapperSs">
    <resultMap id="userDataResultMap" type="com.example.demo.UserData" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="birthY" property="birthY" jdbcType="VARCHAR" />
        <result column="birthM" property="birthM" jdbcType="VARCHAR" />
        <result column="birthD" property="birthD" jdbcType="VARCHAR" />
        <result column="sex" property="sex" jdbcType="VARCHAR" />
        <result column="memo" property="memo" jdbcType="VARCHAR" />
        <result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
    </resultMap>
    <select id="findById" parameterType="java.lang.Long" resultMap="userDataResultMap">
        SELECT
             id
           , name
           , birth_year as birthY
           , birth_month as birthM
           , birth_day as birthD
           , sex
           , memo
           , CASE sex
             WHEN '1' THEN '男'
             WHEN '2' THEN '女'
             ELSE ''
             END AS sex_value
        FROM dbo.USER_DATA
        WHERE id = #{id}
    </select>
    <update id="update" parameterType="com.example.demo.UserData">
        UPDATE dbo.USER_DATA SET name = #{name}, birth_year = #{birthY}
           , birth_month = #{birthM}, birth_day = #{birthD}
           , sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
        WHERE id = #{id}
    </update>
</mapper>



「CODE×CODE」は、需要の高い技術(AWS, Python等)を習得できるプログラミングスクールスクールだった近年、さまざまな会社でクラウド(特にIaaSやPaaSのパブリッククラウド)の需要が非常に高まっていて、クラウドサービスによるシステム開...

さらに、サービスクラスの内容は以下の通りで、前提条件の記事と同じ内容の検証を行う処理を、Oracle用とSQL Server用で作成している。

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;

    /**
     * Oracleのユーザーデータテーブル(user_data)を更新するトランザクション
     */
    // @Transactionalアノテーションで、このメソッド内のOracleのDB更新が全て成功すれば
    // コミットされ、そうでなければロールバックされる
    @Transactional(transactionManager = "txManagerOra")
    public void transUserDataOra() {
        UserData userData = userDataMapperOra.findById(Long.valueOf(1));

        // ユーザーデータが取得できなければ処理を終了する
        if (userData == null) {
            System.out.println("id=1のユーザーデータは見つかりませんでした。");
            return;
        }

        // 更新前データを表示
        System.out.println("ユーザーデータ(更新前) : " + userData.toString());

        // 氏名を更新する
        userData.setName("テスト プリン1 更新後");
        userDataMapperOra.update(userData);

        // 更新後データを表示
        userData = userDataMapperOra.findById(Long.valueOf(1));
        System.out.println("ユーザーデータ(1回目更新後) : " + userData.toString());

        // 氏名を再度更新する
        userData.setName(USER_NAME_OK);
        userDataMapperOra.update(userData);

        // 更新後データを表示
        userData = userDataMapperOra.findById(Long.valueOf(1));
        System.out.println("ユーザーデータ(2回目更新後) : " + userData.toString());
    }

    /**
     * SQL Serverのユーザーデータテーブル(user_data)を更新するトランザクション
     */
    // @Transactionalアノテーションで、このメソッド内のSQLServerのDB更新が全て成功すれば
    // コミットされ、そうでなければロールバックされる
    @Transactional(transactionManager = "txManagerSs")
    public void transUserDataSs() {
        UserData userData = userDataMapperSs.findById(Long.valueOf(1));

        // ユーザーデータが取得できなければ処理を終了する
        if (userData == null) {
            System.out.println("id=1のユーザーデータは見つかりませんでした。");
            return;
        }

        // 更新前データを表示
        System.out.println("ユーザーデータ(更新前) : " + userData.toString());

        // 氏名を更新する
        userData.setName("テスト プリン1 更新後");
        userDataMapperSs.update(userData);

        // 更新後データを表示
        userData = userDataMapperSs.findById(Long.valueOf(1));
        System.out.println("ユーザーデータ(1回目更新後) : " + userData.toString());

        // 氏名を再度更新する
        userData.setName(USER_NAME_OK);
        userDataMapperSs.update(userData);

        // 更新後データを表示
        userData = userDataMapperSs.findById(Long.valueOf(1));
        System.out.println("ユーザーデータ(2回目更新後) : " + userData.toString());
    }

}

また、Spring Bootのメインクラスの内容は以下の通りで、サービスクラスの各メソッドを呼び出している。

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 {
            // ユーザーデータテーブル(user_data)を更新する
            // トランザクションを呼び出す
            // DB更新に失敗するとExceptionがスローされる
            //demoService.transUserDataOra();
            demoService.transUserDataSs();
        } catch (Exception ex) {
            System.err.println(ex);
        }
    }

}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-multi-db-transactional/demo



freelance hubを利用して10万件を超える案件情報からJava Spring案件を検索してみたfreelance hubは、レバテックフリーランスやフリエン(furien)を始めとした多くのフリーランスエージェントの案件をまとめて...

サンプルプログラムの実行結果(Oracleの場合)

サンプルプログラムの実行結果は以下の通りで、Oracleデータベースに接続した場合に、同一トランザクション内でDB更新が成功するとコミット、失敗するとロールバックされることが確認できる。

1) 以下のように、DemoApplicationクラスで、Oracleに接続した際の動作検証を行う処理(demoServiceクラスのtransUserDataOraメソッド)を有効にする。
サンプルプログラムの実行結果(Oracle)_1

2) 以下のように、DemoServiceクラスで、Oracleに接続した際の2回目の氏名更新時の設定値を、更新成功する値に設定する。
サンプルプログラムの実行結果(Oracle)_2

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

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

<実行前>

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

<実行後>

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

5) 以下のように、DemoServiceクラスで、Oracleに接続した際の2回目の氏名更新時の設定値を、更新失敗する値に設定する。
サンプルプログラムの実行結果(Oracle)_5

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

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

<実行前>

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

<実行後>

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



【PR】「Filmora」は初心者でも本格的な動画編集ができる大変便利なツールだった「Filmora」は初心者でも使いやすい動画編集ツールで、テンプレートとして利用できるテキスト・動画・音楽などが充実していると共に、複数...

サンプルプログラムの実行結果(SQL Serverの場合)

サンプルプログラムの実行結果は以下の通りで、SQL Serverデータベースに接続した場合にも、同一トランザクション内でDB更新が成功するとコミット、失敗するとロールバックされることが確認できる。

1) 以下のように、DemoApplicationクラスで、SQL Serverに接続した際の動作検証を行う処理(demoServiceクラスのtransUserDataSsメソッド)を有効にする。
サンプルプログラムの実行結果(SQLServer)_1

2) 以下のように、DemoServiceクラスで、SQL Serverに接続した際の2回目の氏名更新時の設定値を、更新成功する値に設定する。
サンプルプログラムの実行結果(SQLServer)_2

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

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

<実行前>

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

<実行後>

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

5) 以下のように、DemoServiceクラスで、SQL Serverに接続した際の2回目の氏名更新時の設定値を、更新失敗する値に設定する。
サンプルプログラムの実行結果(SQLServer)_5

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

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

<実行前>

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

<実行後>

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

要点まとめ

  • Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、複数DBに接続するアプリケーションの場合は、@Transactionalアノテーション内でtransactionManager属性を指定すればよい。