Spring Boot DB連携

Oracle上で@Transactionalアノテーションをネストして利用してみた

@Transactionalアノテーションはクラス間でネストして利用することができ、propagation属性の設定値を変えることで、今まであるトランザクションを引き継いだり、常に新しいトランザクションを開始したりすることができる。

今回は、@Transactionalアノテーションをクラス間でネストして設定することで、トランザクションのコミット/ロールバックの動作を試してみたので、その結果を共有する。

なお、@Transactionalアノテーションに指定するpropagation属性の設定値については、以下のサイトを参照のこと。
https://qiita.com/NagaokaKenichi/items/a279857cc2d22a35d0dd#propagation

前提条件

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

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

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

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

UserDataMapper.java、UserDataMapper.xmlの内容は以下の通りで、findByIdListメソッドにより、指定したIDリストのIDにあてはまるユーザーデータリストを取得する処理を追加している。

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);
}
<?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>
フリエン(furien)は多くの案件を保有しフリーランス向けサービスも充実しているエージェントだったフリエン(furien)は、ITフリーランス(個人事業主)エンジニア専門のエージェントであるアン・コンサルティング株式会社が運営する業界...

また、サービスクラスの内容は以下の通りで、@Transactionalアノテーションを付与しているサブクラスの各メソッドの呼び出しを行っている。

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パターンで設定している。

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



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

サンプルプログラムの実行結果(DemoServiceクラスで「@Transactional」アノテーションあり)

DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを有効にした場合の実行結果は、以下の通り。

1) 以下のように、DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを有効にする。
サンプルプログラムの実行結果_1_1

2) id=4, id=8, id=10のデータが全て更新成功する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。

2-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_1_2_1

2-2) id=4, id=8, id=10のデータが全て更新成功する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。以下の赤枠は、id=4のデータを更新成功する状態に設定している状態を示している。
サンプルプログラムの実行結果_1_2_2

2-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_1_2_3

2-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、全てのデータのNAMEが更新されていることが確認できる。
サンプルプログラムの実行結果_1_2_4

3) id=4のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=4は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性の指定なし(今まであるトランザクションを引き継ぐ)の状態になっている。

3-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_1_3_1

3-2) id=4のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_1_3_2

3-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_1_3_3

3-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、「@Transactional」アノテーションでpropagation属性の指定のないメソッドで実行した更新処理は、全てロールバックされることが確認できる。
サンプルプログラムの実行結果_1_3_4

4) id=8のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=8は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性がREQUIRES_NEW(常に新しいトランザクションを開始する)の状態になっている。

4-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_1_4_1

4-2) id=8のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_1_4_2

4-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_1_4_3

4-4) Spring Boottのメインクラスを実行した後のデータは、以下の通りで、id=7,id=8のデータを更新しているトランザクションのみがロールバックされることが確認できる。
サンプルプログラムの実行結果_1_4_4

5) id=10のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。

5-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_1_5_1

5-2) id=10のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_1_5_2

5-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_1_5_3

5-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、「@Transactional」アノテーションでpropagation属性の指定のないメソッドで実行した更新処理は、全てロールバックされることが確認できる。
サンプルプログラムの実行結果_1_5_4



「EaseUS Todo Backup」は様々な形でバックアップ取得が行える便利ツールだったパソコン内のデータを、ファイル/パーティション/ディスク等の様々な単位でバックアップしたり、バックアップのスケジュール設定や暗号化設定も...

サンプルプログラムの実行結果(DemoServiceクラスで「@Transactional」アノテーションなし)

DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを無効にした場合の実行結果は、以下の通り。

1) 以下のように、DemoServiceクラスのtransUserDataメソッドの「@Transactional」アノテーションを無効にする。
サンプルプログラムの実行結果_2_1

2) id=4, id=8, id=10のデータが全て更新成功する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。

2-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_2_2_1

2-2) id=4, id=8, id=10のデータが全て更新成功する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。以下の赤枠は、id=4のデータを更新成功する状態に設定している状態を示している。
サンプルプログラムの実行結果_2_2_2

2-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_2_2_3

2-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、全てのデータのNAMEが更新されていることが確認できる。
サンプルプログラムの実行結果_2_2_4

3) id=4のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=4は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性の指定なし(今まであるトランザクションを引き継ぐ)の状態になっている。

3-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_2_3_1

3-2) id=4のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_2_3_2

3-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_2_3_3

3-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、id=3,id=4のデータを更新しているトランザクションのみがロールバックされることが確認できる。
サンプルプログラムの実行結果_2_3_4

4) id=8のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=8は、DemoServiceSubクラスで、「@Transactional」アノテーションでpropagation属性がREQUIRES_NEW(常に新しいトランザクションを開始する)の状態になっている。

4-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_2_4_1

4-2) id=8のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_2_4_2

4-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_2_4_3

4-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、id=7,id=8のデータを更新しているトランザクションのみがロールバックされることが確認できる。
サンプルプログラムの実行結果_2_4_4

5) id=10のデータが更新失敗する状態にした場合の、サンプルプログラムの実行結果は、以下の通り。なお、id=10は、DemoServiceクラスのtransUserDataメソッドで直接、DB更新をするようになっている。

5-1) Spring Bootのメインクラスを実行する前のデータは、以下の通り。
サンプルプログラムの実行結果_2_5_1

5-2) id=10のデータが更新失敗する状態にして、Spring Bootのメインクラス(DemoApplication.java)を実行する。
サンプルプログラムの実行結果_2_5_2

5-3) コンソールログに出力される内容は、以下の通り。
サンプルプログラムの実行結果_2_5_3

5-4) Spring Bootのメインクラスを実行した後のデータは、以下の通りで、更新に失敗したid=10のデータ以外はコミットされていることが確認できる。
サンプルプログラムの実行結果_2_5_4

要点まとめ

  • @Transactionalアノテーションで、propagation属性の指定が無い場合は、トランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用する仕様となる。
  • @Transactionalアノテーションで、propagation属性にREQUIRES_NEWを指定した場合は、常に新しいトランザクションを開始し、トランザクションが存在する場合は中断して新しいトランザクションを開始する仕様となる。