【初心者のトランザクション管理+SpringBoot+Mybatis】トランザクション管理方法のまとめ!

どうも!ヒグッティ(X→ヒグッティ@システムエンジニア)です!
今回は、SpringBootとMybatisでトランザクション管理の方法をまとめていきます!!そもそもトランザクションって何?という方もいるはずです!トランザクション管理はどんなプロジェクトでも避けて通るこのできない重要な要素です。これを理解できればエンジニアとしてかなりレベルアップします!多分!逆に理解していないとバグの温床に、、今回はソース内に明示的にトランザクション管理のコードを記載するようにしました!初歩的な方法です!最近はフレームワーク側で全て自動でやっていることも多いのでたまには初心に立ち返ってはどうでしょう!

今回、使うソースは以下の記事で利用したものになります。まだ見ていない方は是非見てください!

https://www.higutthiengineer.com/2024/06/06/springbootmybatiselectinlike/

トランザクションとは?

一言で言うと「データの整合性を担保すること」です。アプリケーションからDBを操作してテーブルのデータを検索、登録、更新、削除したりします。その途中で予期せぬエラーが起きて途中で処理を中断せざるおえないことがあります。その時にトランザクション管理をしていると、処理を開始する前の状態にDBのデータを戻すことができます。

SpringBootでのトランザクション管理方法

SpringBootでトランザクションを管理する場合、主に以下の方法があります。

  • PlatformTransactionManagerクラスの利用
  • SqlSessionクラスの利用
  • TransactionTemplateクラスの利用

百聞は一見にしかずなので、まずは作成したサンプルソースを見てみましょう!http://localhost:8080/tran/addplatなどとアクセスすると動作するようになっています。今回はエラーが発生するようにRuntimeExceptionを必ず発生させロールバックするようにしています。

package com.example.demo;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.domain.Userhi;
import com.example.demo.repo.UserhiRepo;

@RestController
@RequestMapping("/tran")
public class Util {
	@Autowired
	PlatformTransactionManager txManager;
	@Autowired
	SqlSessionFactory factory;
	@Autowired
	UserhiRepo userRepo;

	@GetMapping("/addplat")
	public Map<String, String> plat() {
		final Map<String, String> map = new HashMap<String, String>();
		DefaultTransactionDefinition def = new DefaultTransactionDefinition();
		def.setName("HiguTran");
		def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

		TransactionStatus status = txManager.getTransaction(def);
		try {
			LocalDateTime now = LocalDateTime.now();
			Userhi u = new Userhi();
			u.setUserEmail("higuhigu@tran.com");
			u.setUserName("higutran");
			u.setDelFlg(0);
			u.setUserRegDatetime(now);
			u.setUserUpdateStamp(now);
			userRepo.create(u);
			boolean res = true;
			if (res) {
				throw new RuntimeException();
			}
		} catch (Exception ex) {
			txManager.rollback(status);
			map.put("PlatformTransactionManager error", "error PlatformTransactionManager!");

			return map;

		}
		txManager.commit(status);
		return map;

	}

	@GetMapping("/addsql")
	public Map<String, String> sql() {
		final Map<String, String> map = new HashMap<String, String>();

		SqlSession session = factory.openSession();
        try {
        	UserhiRepo ur = session.getMapper(UserhiRepo.class);
			LocalDateTime now = LocalDateTime.now();
			Userhi u = new Userhi();
			u.setUserEmail("higuhigu@tran.com");
			u.setUserName("higutran");
			u.setDelFlg(0);
			u.setUserRegDatetime(now);
			u.setUserUpdateStamp(now);
			userRepo.create(u);
			boolean res = true;
			if (res) {
				throw new RuntimeException();
			}
            session.commit();
        } catch(Exception e ) {
            session.rollback();
			map.put("SqlSession error", "error SqlSession!");
        } finally {
            session.close();
        }

		return map;
	}
	@GetMapping("/addtran")
	public Map<String, String> tran() {
		final Map<String, String> map = new HashMap<String, String>();
		TransactionTemplate temp = new TransactionTemplate(txManager);
		temp.execute(new TransactionCallbackWithoutResult() {
			protected void doInTransactionWithoutResult(TransactionStatus status) {
				try {
					LocalDateTime now = LocalDateTime.now();
					Userhi u = new Userhi();
					u.setUserEmail("higuhigu@tran.com");
					u.setUserName("higutran");
					u.setDelFlg(0);
					u.setUserRegDatetime(now);
					u.setUserUpdateStamp(now);
					userRepo.create(u);
					if (true) {
						throw new RuntimeException();
					}

				} catch (Exception e) {
					status.setRollbackOnly();
					map.put("TransactionTemplate error", "error TransactionTemplate!");
				}

			}
		});
		return map;
	}
}

PlatformTransactionManagerクラスを使った処理が上記サンプルのplat()メソッドです。
SqlSessionクラスを使った処理が上記サンプルのsql()メソッドです。
TransactionTemplateクラスを使った処理が上記サンプルのtran()メソッドです。

では1つずつ見ていきましょう!

PlatformTransactionManager

Springが提供するトランザクション管理のモジュールです。Springの公式ドキュメントによるとPlatformTransactionManagerを単体で使うことは推奨していないようです。後述するTransactionTemplateと組み合わせて使うらしいです。以下、公式ドキュメントです!

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html

実際にPlatformTransactionManagerを使ったソースを見てみましょう!

~省略~
  @GetMapping("/addplat")
	public Map<String, String> plat() {
		final Map<String, String> map = new HashMap<String, String>();
		DefaultTransactionDefinition def = new DefaultTransactionDefinition();
		def.setName("HiguTran");
		def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

		TransactionStatus status = txManager.getTransaction(def);
		try {
			LocalDateTime now = LocalDateTime.now();
			Userhi u = new Userhi();
			u.setUserEmail("higuhigu@tran.com");
			u.setUserName("higutran");
			u.setDelFlg(0);
			u.setUserRegDatetime(now);
			u.setUserUpdateStamp(now);
			userRepo.create(u);
			boolean res = true;
			if (res) {
				throw new RuntimeException();
			}
		} catch (Exception ex) {
			txManager.rollback(status);
			map.put("PlatformTransactionManager error", "error PlatformTransactionManager!");
			return map;
		}
		txManager.commit(status);
		return map;
	}
~省略~

txManager.getTransaction(def)でトランザクション管理を開始しています(トランザクションをはる!とも言う)。エラーが発生しなければtxManager.commit(status)でinsertした結果が保存されます!エラーが発生してcatchされるとtxManager.rollback(status)によりデータがロールバックされます。簡単ですね。
ちなみにdef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED)では何をしているの?と思うかもいると思いますので重要なので簡単に説明します。この設定はトランザクションの伝搬属性といわれます。

伝搬属性

一言で言うと「トランザクションの管理方法」です。処理が大きく複雑化してくるとトランザクションが複数になる場合があります。そのため、トランザクションを開始する時に既にトランザクションが始まっていることもあります。その時にどんなトランザクション管理をするかという設定が伝搬属性になります。

以下がトランザクションの伝搬属性の例です!

ここでは2つ説明します。1つはサンプルソースにあったPROPAGATION_REQUIREDです。これはトランザクション開始する時にすでにトランザクションが存在していれば既存のトランザクションを利用します!なければ新しくトランザクションを作ります!

PROPAGATION_REQUIRES_NEWというのもあります。これを指定すると、既にトランザクションが存在していても新しくトランザクションを作成します。以下説明になります!

上記の違いはエラーになった時の動作です。PROPAGATION_REQUIREDは処理1、処理2のどこでエラーになっても処理1のトランザクション開始前の状態に戻ります!PROPAGATION_REQUIRES_NEWは処理1でエラーになり処理2が正常に終了した場合、処理1のみトランザクション開始前の状態に戻ります。処理2で変更した内容は戻りません!

通常はトランザクションは処理の中で1つだけ用意するのが鉄則のような気もしますが、スレッドとか作るとね、、そうも言ってられないのかなwww

SqlSession

SqlSessionはMyBatis-Springというライブラリの中にあるトランザクション管理のクラスです。基本的にはMyBatisが用意してくれたクラスと思っ他方が良いかも!実際にソースを見てみましょう!

~省略~
	@GetMapping("/addsql")
	public Map<String, String> sql() {
		final Map<String, String> map = new HashMap<String, String>();

		SqlSession session = factory.openSession();
    try {
      UserhiRepo ur = session.getMapper(UserhiRepo.class);
			LocalDateTime now = LocalDateTime.now();
			Userhi u = new Userhi();
			u.setUserEmail("higuhigu@tran.com");
			u.setUserName("higutran");
			u.setDelFlg(0);
			u.setUserRegDatetime(now);
			u.setUserUpdateStamp(now);
			userRepo.create(u);
			boolean res = true;
			if (res) {
				throw new RuntimeException();
			}
      session.commit();
    } catch(Exception e ) {
      session.rollback();
			map.put("SqlSession error", "error SqlSession!");
    } finally {
        session.close();
    }

		return map;
	}
~省略~

使い方はあまり変わりません!上のソースで言うとfactory.openSession()でトランザクションを開始し、session.commit()でコミットしています!エラーが発生した場合はsession.rollback()でロールバックしています。

TransactionTemplate

最後にTransactionTemplateの使い方です。PlatformTransactionManagerの使い方で少し触れましたが、 PlatformTransactionManagerを便利にしたものがTransactionTemplateです!ソースを見てみましょう!

~省略~
	@Autowired
	PlatformTransactionManager txManager;
~省略~
	@GetMapping("/addtran")
	public Map<String, String> tran() {
		final Map<String, String> map = new HashMap<String, String>();
		TransactionTemplate temp = new TransactionTemplate(txManager);
		temp.execute(new TransactionCallbackWithoutResult() {
			protected void doInTransactionWithoutResult(TransactionStatus status) {
				try {
					LocalDateTime now = LocalDateTime.now();
					Userhi u = new Userhi();
					u.setUserEmail("higuhigu@tran.com");
					u.setUserName("higutran");
					u.setDelFlg(0);
					u.setUserRegDatetime(now);
					u.setUserUpdateStamp(now);
					userRepo.create(u);
					if (true) {
						throw new RuntimeException();
					}
				} catch (Exception e) {
					status.setRollbackOnly();
					map.put("TransactionTemplate error", "error TransactionTemplate!");
				}
			}
		});
		return map;
	}
~省略~

何が便利になったのかわかりずらいですが、無名関数(ラムダ関数でもOK)の中に処理を記載して処理を分けることができます。今回は呼び出し元と同じメソッド内に処理を書いていますが、別のクラスに切り出すことが簡単にできるようになっています。コミットやロールバックは他の使い方とほぼ同じです!

まとめ

トランザクション管理はプログラミングにおいて重要な要素の一つです!プロジェクトによっては今回紹介したように明示的にトランザクションを記載しているコードも多くあるかもしれません!色々な書き方があるので最初はどうすればいいの!?と思ってしまいますが、整理するとそれぞれの書き方に長所や短所が見えてきて面白かったですね!次は大量のデータを登録する時の方法などを記事にしようかな~
最近、ネタ切れでJavaに活路を求めていますww是非、検証してほしいものがあればXのDM待っています!! ヒグッティ@システムエンジニア

スポンサーリンク