Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、複数DBに接続するアプリケーションの場合は、@Transactionalアノテーション内でtransactionManager属性を指定すればよい。
今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、OracleやSQL Serverに接続し@Transactionalアノテーションの挙動を調べてみたので、共有する。
前提条件
下記記事の実装が完了していること。
また、下記記事の前提条件を満たしていること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから追加/変更したプログラムである。
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
また、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();
}
}
各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>
さらに、サービスクラスの内容は以下の通りで、前提条件の記事と同じ内容の検証を行う処理を、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
サンプルプログラムの実行結果(Oracleの場合)
サンプルプログラムの実行結果は以下の通りで、Oracleデータベースに接続した場合に、同一トランザクション内でDB更新が成功するとコミット、失敗するとロールバックされることが確認できる。
1) 以下のように、DemoApplicationクラスで、Oracleに接続した際の動作検証を行う処理(demoServiceクラスのtransUserDataOraメソッド)を有効にする。

2) 以下のように、DemoServiceクラスで、Oracleに接続した際の2回目の氏名更新時の設定値を、更新成功する値に設定する。

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

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

<実行後>
select * from user_data where id = 1

5) 以下のように、DemoServiceクラスで、Oracleに接続した際の2回目の氏名更新時の設定値を、更新失敗する値に設定する。

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

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

<実行後>
select * from user_data where id = 1

サンプルプログラムの実行結果(SQL Serverの場合)
サンプルプログラムの実行結果は以下の通りで、SQL Serverデータベースに接続した場合にも、同一トランザクション内でDB更新が成功するとコミット、失敗するとロールバックされることが確認できる。
1) 以下のように、DemoApplicationクラスで、SQL Serverに接続した際の動作検証を行う処理(demoServiceクラスのtransUserDataSsメソッド)を有効にする。

2) 以下のように、DemoServiceクラスで、SQL Serverに接続した際の2回目の氏名更新時の設定値を、更新成功する値に設定する。

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

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

<実行後>
select * from dbo.user_data where id = 1

5) 以下のように、DemoServiceクラスで、SQL Serverに接続した際の2回目の氏名更新時の設定値を、更新失敗する値に設定する。

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

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

<実行後>
select * from dbo.user_data where id = 1

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





