@Transactionalアノテーションはクラス間でネストして利用することができ、propagation属性の設定値を変えることで、今まであるトランザクションを引き継いだり、常に新しいトランザクションを開始したりすることができる。
今回は、@Transactionalアノテーションをクラス間でネストして設定することで、トランザクションのコミット/ロールバックの動作を試してみたので、その結果を共有する。
なお、@Transactionalアノテーションに指定するpropagation属性の設定値については、以下のサイトを参照のこと。
https://qiita.com/NagaokaKenichi/items/a279857cc2d22a35d0dd#propagation
前提条件
下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
UserDataMapper.java、UserDataMapper.xmlの内容は以下の通りで、findByIdListメソッドにより、指定したIDリストのIDにあてはまるユーザーデータリストを取得する処理を追加している。
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 | package com.example.demo; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface UserDataMapper { /** * 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する * @param id ID * @return ユーザーデータテーブル(user_data)の指定したIDのデータ */ UserData findById(Long id); /** * 指定したユーザーデータテーブル(user_data)のデータを更新する * @param userData ユーザーデータテーブル(user_data)の更新データ */ void update(UserData userData); /** * 指定したIDリストのIDにあてはまるユーザーデータテーブル(user_data)の * データを取得する * @param idList IDリスト * @return ユーザーデータテーブル(user_data)のリスト */ List<UserData> findByIdList(List<Long> idList); } |
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 | <?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.UserDataMapper"> <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> <select id="findByIdList" parameterType="java.util.List" 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> <if test="list != null and list.size() > 0"> id in <foreach item="item" open="(" close=")" collection="list" separator=","> #{item} </foreach> </if> </where> ORDER BY id ASC </select> </mapper> |
また、サービスクラスの内容は以下の通りで、@Transactionalアノテーションを付与しているサブクラスの各メソッドの呼び出しを行っている。
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 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @Service public class DemoService { /** * ユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapper userDataMapper; /** * ユーザーデータテーブルの更新を行うサービス */ @Autowired private DemoSubService demoSubService; /** * ユーザーデータテーブル(user_data)を更新するトランザクション */ @Transactional public void transUserData() { System.out.println("com.example.demo.DemoService.transUserData start."); System.out.println(); // id=1~10までをもつユーザーデータリストを取得する List<Long> idList = new ArrayList<>(); for(long i = 1; i == 10; i++){ idList.add(i); } List<UserData> userDataList = userDataMapper.findByIdList(idList); // ユーザーデータリストのレコード数が10件でなければ処理を終了する if(userDataList == null || userDataList.size() != 10){ return; } // ユーザーデータ1、ユーザーデータ2を更新する(@Transactionalアノテーションで // propagationの指定がない場合)のメソッドを呼びだし demoSubService.transUserDataSub1(userDataList.get(0), userDataList.get(1)); // ユーザーデータ3、ユーザーデータ4を更新する(@Transactionalアノテーションで // propagationの指定がない場合)のメソッドを呼びだし // エラーを発生させる場合もある try{ demoSubService.transUserDataSub2(userDataList.get(2), userDataList.get(3)); }catch(Exception ex){ System.err.println(ex); } // ユーザーデータ5、ユーザーデータ6を更新する(@Transactionalアノテーションで // propagationにREQUIRES_NEWを指定した場合)のメソッドを呼びだし demoSubService.transUserDataSub3(userDataList.get(4), userDataList.get(5)); // ユーザーデータ7、ユーザーデータ8を更新する(@Transactionalアノテーションで // propagationにREQUIRES_NEWを指定した場合)のメソッドを呼びだし // エラーを発生させる場合もある try{ demoSubService.transUserDataSub4(userDataList.get(6), userDataList.get(7)); }catch(Exception ex){ System.err.println(ex); } // ユーザーデータ9、ユーザーデータ10を更新する // エラーを発生させる場合もある // 更新前データを表示 System.out.println("ユーザーデータ9(更新前) : " + userDataList.get(8).toString()); System.out.println("ユーザーデータ10(更新前) : " + userDataList.get(9).toString()); userDataList.get(8).setName(DemoSubService.USER_NAME_OK); userDataMapper.update(userDataList.get(8)); userDataList.get(9).setName(DemoSubService.USER_NAME_OK); //userDataList.get(9).setName(DemoSubService.USER_NAME_NG); userDataMapper.update(userDataList.get(9)); // 更新後データを表示 System.out.println("ユーザーデータ9(更新後) : " + userDataList.get(8).toString()); System.out.println("ユーザーデータ10(更新後) : " + userDataList.get(9).toString()); System.out.println("com.example.demo.DemoService.transUserData end."); } } |
さらに、サービスクラスのサブクラスの内容は以下の通りで、@Transactionalアノテーションのpropagation属性の設定値を2パターンで設定している。
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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | package com.example.demo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class DemoSubService { /** ユーザーの更新後氏名を定義 */ /** USER_NAME_OK:13桁39バイトで更新OK、USER_NAME_NG:14桁42バイトで更新NG */ public static String USER_NAME_OK = "1234567890123"; public static String USER_NAME_NG = "12345678901234"; /** * ユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapper userDataMapper; /** * ユーザーデータ1, ユーザーデータ2を更新するメソッド * @param userData1 ユーザーデータ1 * @param userData2 ユーザーデータ2 */ // @Transactionalアノテーションでpropagationの指定がない場合:REQUIRED // トランザクションが開始されていなければ新規に開始し、すでに開始されていれば // そのトランザクションをそのまま利用する @Transactional public void transUserDataSub1(UserData userData1, UserData userData2){ System.out.println("com.example.demo.DemoSubService.transUserDataSub1 start."); // 更新前データを表示 System.out.println("ユーザーデータ1(更新前) : " + userData1.toString()); System.out.println("ユーザーデータ2(更新前) : " + userData2.toString()); userData1.setName(USER_NAME_OK); userDataMapper.update(userData1); userData2.setName(USER_NAME_OK); userDataMapper.update(userData2); // 更新後データを表示 System.out.println("ユーザーデータ1(更新後) : " + userData1.toString()); System.out.println("ユーザーデータ2(更新後) : " + userData2.toString()); System.out.println("com.example.demo.DemoSubService.transUserDataSub1 end."); } /** * ユーザーデータ3, ユーザーデータ4を更新するメソッド * @param userData3 ユーザーデータ3 * @param userData4 ユーザーデータ4 */ @Transactional public void transUserDataSub2(UserData userData3, UserData userData4){ System.out.println("com.example.demo.DemoSubService.transUserDataSub2 start."); // 更新前データを表示 System.out.println("ユーザーデータ3(更新前) : " + userData3.toString()); System.out.println("ユーザーデータ4(更新前) : " + userData4.toString()); userData3.setName(USER_NAME_OK); userDataMapper.update(userData3); userData4.setName(USER_NAME_OK); //userData4.setName(USER_NAME_NG); userDataMapper.update(userData4); // 更新後データを表示 System.out.println("ユーザーデータ3(更新後) : " + userData3.toString()); System.out.println("ユーザーデータ4(更新後) : " + userData4.toString()); System.out.println("com.example.demo.DemoSubService.transUserDataSub2 end."); } /** * ユーザーデータ5, ユーザーデータ6を更新するメソッド * @param userData5 ユーザーデータ5 * @param userData6 ユーザーデータ6 */ // @TransactionalアノテーションでpropagationにREQUIRES_NEWを指定した場合 // 常に新しいトランザクションを開始する // トランザクションが存在する場合は中断して新しいトランザクションを開始する @Transactional(propagation = Propagation.REQUIRES_NEW) public void transUserDataSub3(UserData userData5, UserData userData6){ System.out.println("com.example.demo.DemoSubService.transUserDataSub3 start."); // 更新前データを表示 System.out.println("ユーザーデータ5(更新前) : " + userData5.toString()); System.out.println("ユーザーデータ6(更新前) : " + userData6.toString()); userData5.setName(USER_NAME_OK); userDataMapper.update(userData5); userData6.setName(USER_NAME_OK); userDataMapper.update(userData6); // 更新後データを表示 System.out.println("ユーザーデータ5(更新後) : " + userData5.toString()); System.out.println("ユーザーデータ6(更新後) : " + userData6.toString()); System.out.println("com.example.demo.DemoSubService.transUserDataSub3 end."); } /** * ユーザーデータ7, ユーザーデータ8を更新するメソッド * @param userData7 ユーザーデータ7 * @param userData8 ユーザーデータ8 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void transUserDataSub4(UserData userData7, UserData userData8){ System.out.println("com.example.demo.DemoSubService.transUserDataSub4 start."); // 更新前データを表示 System.out.println("ユーザーデータ7(更新前) : " + userData7.toString()); System.out.println("ユーザーデータ8(更新前) : " + userData8.toString()); userData7.setName(USER_NAME_OK); userDataMapper.update(userData7); userData8.setName(USER_NAME_OK); //userData8.setName(USER_NAME_NG); userDataMapper.update(userData8); // 更新後データを表示 System.out.println("ユーザーデータ7(更新後) : " + userData7.toString()); System.out.println("ユーザーデータ8(更新後) : " + userData8.toString()); System.out.println("com.example.demo.DemoSubService.transUserDataSub4 end."); } } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-oracle-transactional-nest/demo
サンプルプログラムの実行結果(DemoServiceクラスで「@Transactional」アノテーションあり)
DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを有効にした場合の実行結果は、以下の通り。
1) 以下のように、DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを有効にする。
2) id=4, id=8, id=10のデータが全て更新成功する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。
2-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
2-2) id=4, id=8, id=10のデータが全て更新成功する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。以下の赤枠は、id=4のデータを更新成功する状態に設定している状態を示している。
2-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、全てのデータのNAMEが更新されていることが確認できる。
3) id=4のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=4は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性の指定なし(今まであるトランザクションを引き継ぐ)の状態になっている。
3-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
3-2) id=4のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
3-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、「@Transactional」アノテーションでpropagation属性の指定のないメソッドで実行した更新処理は、全てロールバックされることが確認できる。
4) id=8のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=8は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性がREQUIRES_NEW(常に新しいトランザクションを開始する)の状態になっている。
4-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
4-2) id=8のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
4-4) Spring Boottのメインクラスを実行した後のデータは、以下の通りで、id=7,id=8のデータを更新しているトランザクションのみがロールバックされることが確認できる。
5) id=10のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。
5-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
5-2) id=10のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
5-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、「@Transactional」アノテーションでpropagation属性の指定のないメソッドで実行した更新処理は、全てロールバックされることが確認できる。
サンプルプログラムの実行結果(DemoServiceクラスで「@Transactional」アノテーションなし)
DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを無効にした場合の実行結果は、以下の通り。
1) 以下のように、DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを無効にする。
2) id=4, id=8, id=10のデータが全て更新成功する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。
2-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
2-2) id=4, id=8, id=10のデータが全て更新成功する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。以下の赤枠は、id=4のデータを更新成功する状態に設定している状態を示している。
2-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、全てのデータのNAMEが更新されていることが確認できる。
3) id=4のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=4は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性の指定なし(今まであるトランザクションを引き継ぐ)の状態になっている。
3-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
3-2) id=4のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
3-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、id=3,id=4のデータを更新しているトランザクションのみがロールバックされることが確認できる。
4) id=8のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=8は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性がREQUIRES_NEW(常に新しいトランザクションを開始する)の状態になっている。
4-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
4-2) id=8のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
4-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、id=7,id=8のデータを更新しているトランザクションのみがロールバックされることが確認できる。
5) id=10のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=10は、DemoServiceクラスのtransUserDataメソッドで直接、DB更新をするようになっている。
5-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
5-2) id=10のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
5-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、更新に失敗したid=10のデータ以外はコミットされていることが確認できる。
要点まとめ
- @Transactionalアノテーションで、propagation属性の指定が無い場合は、トランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用する仕様となる。
- @Transactionalアノテーションで、propagation属性にREQUIRES_NEWを指定した場合は、常に新しいトランザクションを開始し、トランザクションが存在する場合は中断して新しいトランザクションを開始する仕様となる。