Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、複数DBに接続するアプリケーションの場合は、@Transactionalアノテーション内でtransactionManager属性を指定すればよい。
今回は、Spring Bootアプリケーション内でMyBatisフレームワークを利用する状態で、OracleやSQL Serverに接続し@Transactionalアノテーションの挙動を調べてみたので、共有する。
前提条件
下記記事の実装が完了していること。
また、下記記事の前提条件を満たしていること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加/変更したプログラムである。
build.gradleの内容は以下の通りで、SQL Serverに接続するためのJDBCライブラリを追加している。
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 | 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に接続するための接続設定を追加している。
1 2 3 4 5 6 7 8 9 10 11 | 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接続に必要なデータソースプロパティ、データソース、トランザクションマネージャ、セッションファクトリを生成している。
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 | 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接続に必要なリソースを生成している。
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 | 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で指定したパッケージ内に、それぞれ作成している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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を記載している。
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 | <?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> |
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 | <?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用で作成している。
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 | 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のメインクラスの内容は以下の通りで、サービスクラスの各メソッドを呼び出している。
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 | 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テーブルの値を確認した結果は以下の通りで、コミットされ氏名が更新されることが確認できる。
<実行前>
1 | select * from user_data where id = 1 |
<実行後>
1 | select * from user_data where id = 1 |
5) 以下のように、DemoServiceクラスで、Oracleに接続した際の2回目の氏名更新時の設定値を、更新失敗する値に設定する。
6) 1)5)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通り。
7) 1)5)の状態で、実行前後でUSER_DATAテーブルの値を確認した結果は以下の通りで、ロールバックされ氏名が更新されないことが確認できる。
<実行前>
1 | select * from user_data where id = 1 |
<実行後>
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テーブルの値を確認した結果は以下の通りで、コミットされ氏名が更新されることが確認できる。
<実行前>
1 | select * from dbo.user_data where id = 1 |
<実行後>
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テーブルの値を確認した結果は以下の通りで、ロールバックされ氏名が更新されないことが確認できる。
<実行前>
1 | select * from dbo.user_data where id = 1 |
<実行後>
1 | select * from dbo.user_data where id = 1 |
要点まとめ
- Spring Bootを利用したアプリケーションでDB接続を利用する際、@Transactionalアノテーションをつけたメソッド内でDB更新処理を実装するようにすると、そのメソッド単位で、DB更新処理が成功した場合にコミットし、失敗した場合にロールバックをすることができるが、複数DBに接続するアプリケーションの場合は、@Transactionalアノテーション内でtransactionManager属性を指定すればよい。