Spring Boot DB連携

Oracle上でSpring Bootの@Transactionalアノテーションの挙動を調べてみた

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

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

前提条件

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

OracleのIN句で1000件を超える項目を指定してみたOracleデータベースの場合、IN句で1000件を超える項目を指定するとエラーになってしまうので、注意が必要である。 今回は、S...

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

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

USER_DATAテーブルのデータを格納するエンティティクラスの内容は以下の通り。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.example.demo;
import lombok.Data;
/**
* ユーザーデータテーブル(user_data)アクセス用エンティティ
*/
@Data
public class UserData {
/** ID */
private long id;
/** 名前 */
private String name;
/** 生年月日_年 */
private int birthY;
/** 生年月日_月 */
private int birthM;
/** 生年月日_日 */
private int birthD;
/** 性別 */
private String sex;
/** メモ */
private String memo;
/** 性別(文字列) */
private String sex_value;
}
package com.example.demo; import lombok.Data; /** * ユーザーデータテーブル(user_data)アクセス用エンティティ */ @Data public class UserData { /** ID */ private long id; /** 名前 */ private String name; /** 生年月日_年 */ private int birthY; /** 生年月日_月 */ private int birthM; /** 生年月日_日 */ private int birthD; /** 性別 */ private String sex; /** メモ */ private String memo; /** 性別(文字列) */ private String sex_value; }
package com.example.demo;

import lombok.Data;

/**
 * ユーザーデータテーブル(user_data)アクセス用エンティティ
 */
@Data
public class UserData {

    /** ID */
    private long id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private int birthY;

    /** 生年月日_月 */
    private int birthM;

    /** 生年月日_日 */
    private int birthD;

    /** 性別 */
    private String sex;

    /** メモ */
    private String memo;

    /** 性別(文字列) */
    private String sex_value;

}

また、UserDataMapper.java、UserDataMapper.xmlの内容は以下の通りで、データ取得・データ更新を行うためのSQLを定義している。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.example.demo;
import org.apache.ibatis.annotations.Mapper;
@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);
}
package com.example.demo; import org.apache.ibatis.annotations.Mapper; @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); }
package com.example.demo;

import org.apache.ibatis.annotations.Mapper;

@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);
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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>
</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.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> </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.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>
</mapper>



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

さらに、サービスクラスの内容は以下の通りで、transUserDataメソッド内でDB更新を2回実行していて、@Transactionalアノテーションを付与している。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
package com.example.demo;
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:14桁42バイトで更新NG */
private static String USER_NAME_OK = "1234567890123";
private static String USER_NAME_NG = "12345678901234";
/**
* ユーザーデータテーブル(user_data)へアクセスするマッパー
*/
@Autowired
private UserDataMapper userDataMapper;
/**
* ユーザーデータテーブル(user_data)を更新するトランザクション
*/
// @Transactionalアノテーションで、このメソッド内のDB更新が全て成功すれば
// コミットされ、そうでなければロールバックされる
@Transactional
public void transUserData() {
UserData userData = userDataMapper.findById(Long.valueOf(1));
// ユーザーデータが取得できなければ処理を終了する
if (userData == null) {
System.out.println("id=1のユーザーデータは見つかりませんでした。");
return;
}
// 更新前データを表示
System.out.println("ユーザーデータ(更新前) : " + userData.toString());
// 氏名を更新する
userData.setName("テスト プリン1 更新後");
userDataMapper.update(userData);
// 更新後データを表示
userData = userDataMapper.findById(Long.valueOf(1));
System.out.println("ユーザーデータ(1回目更新後) : " + userData.toString());
// 氏名を再度更新する
userData.setName(USER_NAME_OK);
userDataMapper.update(userData);
// 更新後データを表示
userData = userDataMapper.findById(Long.valueOf(1));
System.out.println("ユーザーデータ(2回目更新後) : " + userData.toString());
}
}
package com.example.demo; 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:14桁42バイトで更新NG */ private static String USER_NAME_OK = "1234567890123"; private static String USER_NAME_NG = "12345678901234"; /** * ユーザーデータテーブル(user_data)へアクセスするマッパー */ @Autowired private UserDataMapper userDataMapper; /** * ユーザーデータテーブル(user_data)を更新するトランザクション */ // @Transactionalアノテーションで、このメソッド内のDB更新が全て成功すれば // コミットされ、そうでなければロールバックされる @Transactional public void transUserData() { UserData userData = userDataMapper.findById(Long.valueOf(1)); // ユーザーデータが取得できなければ処理を終了する if (userData == null) { System.out.println("id=1のユーザーデータは見つかりませんでした。"); return; } // 更新前データを表示 System.out.println("ユーザーデータ(更新前) : " + userData.toString()); // 氏名を更新する userData.setName("テスト プリン1 更新後"); userDataMapper.update(userData); // 更新後データを表示 userData = userDataMapper.findById(Long.valueOf(1)); System.out.println("ユーザーデータ(1回目更新後) : " + userData.toString()); // 氏名を再度更新する userData.setName(USER_NAME_OK); userDataMapper.update(userData); // 更新後データを表示 userData = userDataMapper.findById(Long.valueOf(1)); System.out.println("ユーザーデータ(2回目更新後) : " + userData.toString()); } }
package com.example.demo;

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:14桁42バイトで更新NG */
    private static String USER_NAME_OK = "1234567890123";
    private static String USER_NAME_NG = "12345678901234";

    /**
     * ユーザーデータテーブル(user_data)へアクセスするマッパー
     */
    @Autowired
    private UserDataMapper userDataMapper;

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

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

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

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

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

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

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

}



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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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.transUserData();
} catch (Exception ex) {
System.err.println(ex);
}
}
}
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.transUserData(); } catch (Exception ex) { System.err.println(ex); } } }
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.transUserData();
        } catch (Exception ex) {
            System.err.println(ex);
        }
    }

}

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



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

サンプルプログラムの実行結果

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

1) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、更新成功する値に設定する。
サンプルプログラムの実行結果_1

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

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

<実行前>

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
select * from user_data where id = 1
select * from user_data where id = 1
select * from user_data where id = 1
サンプルプログラムの実行結果_3_1

<実行後>

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
select * from user_data where id = 1
select * from user_data where id = 1
select * from user_data where id = 1
サンプルプログラムの実行結果_3_2



Code VillageはJavaScriptを中心としたサポート体制が充実したプログラミングスクールだったJavaScriptや、JavaScriptのフレームワーク「React」「Vue」を中心にオンラインで学習できるプログラミングスクール...

4) 以下のように、DemoServiceクラスの2回目の氏名更新時の設定値を、更新失敗する値に設定する。
サンプルプログラムの実行結果_4

5) 4)の状態でSpring Bootのメインクラス(DemoApplication.java)を実行した結果、コンソールログに出力される内容は以下の通りで、DB更新エラーが発生していることが確認できる。
サンプルプログラムの実行結果_5

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

<実行前>

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
select * from user_data where id = 1
select * from user_data where id = 1
select * from user_data where id = 1
サンプルプログラムの実行結果_6_1

<実行後>

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
select * from user_data where id = 1
select * from user_data where id = 1
select * from user_data where id = 1
サンプルプログラムの実行結果_6_2

要点まとめ

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