Simkell

Springでトランザクション処理を理解する

現場で使えるシリーズ。今回はトランザクション処理についてまとめます。
現場ではメソッドにズラッと長いロジックが書かれていて
何度もDBにアクセスしたり、アップデートしたり、多くの処理を一つのメソッドから行うことがあります。

そんな時に毎回毎回コミットするのではなく、処理が終わったらコミットするようなことがあります。
また処理に失敗したらロールバックするようなことをします。

毎回毎回コミットしていると、ロールバックできないので、トランザクションをという機能を利用します。

トランザクション自体は特別な物ではないのですが、場合によって致命的な欠陥を生むことがありますし、何よりデバッグがうまくできなかったりします。
このトランザクションを知らないと、まともにデバッグができないこともありますので、知っとくといいです。

PLSQLでも自律型トランザクションというものがありますが、下手にテストで扱うと環境のデータを更新してしまうこともあります。

今回はトランザクションの動きとデバッグ時の挙動についてまとめていきます。

今回はテーブルのアップデート処理を追加します。
まずは普通にアップデート処理を書きます。

やたらと階層化されているので修正個所が多いですが、量はさほど多くないのでやっていきます。

前回同様のことをします。今回はボタンを押した時にupdateをかけるようにします。
Daoの修正→Serviceの修正→Controllerの修正

Daoのインターフェース

package com.example.demo.dao;

import java.util.List;
import com.example.demo.entity.Talent;

public interface TalentDao{

	public List<Talent> getTalent();
	//追加
	public void updateTalent(Talent talent);
	
}

Daoの実装クラス

package com.example.demo.controllers;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.example.demo.dao.TalentDaoImpl;
import com.example.demo.entity.Talent;
import com.example.demo.service.TalentService;

@Controller
public class SampleController {
	
	//利用するサービスクラスを宣言して置く
	private TalentService talentService;
	
	@Autowired
	public SampleController(TalentService ts) {
		this.talentService = ts;
	}
	
	@GetMapping(value="/sample")
	public ModelAndView home(ModelAndView modelAndView) {
		modelAndView.setViewName("sample");
		modelAndView.addObject("sample","サンプルテキスト");

		List<Talent> list = talentService.getTalent();
		modelAndView.addObject("talents",list);
		
		return modelAndView;
	}
	
	
	@PostMapping(value="/sample")
	public ModelAndView post(@RequestParam("name")String name,ModelAndView modelAndView) {
		modelAndView.setViewName("home");
		modelAndView.addObject("name",name);
		
		//追加
		//適当にタレントを作ってアップデートを呼びます
		Talent talent = new Talent();
		talent.setId(1);
		talent.setName("bomberman");
		
		talentService.updateTalent(talent);
		
		return modelAndView;
	}
	
}

serviceのインターフェース

package com.example.demo.service;

import java.util.List;

import com.example.demo.entity.Talent;

public interface TalentService {
	 List<Talent> getTalent();
	 //追加
	 void updateTalent(Talent talent);
}

service実装クラス

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.dao.TalentDao;
import com.example.demo.entity.Talent;

@Service
public class TalentServiceImpl implements TalentService{
	
	private TalentDao talentDao;
	
	// impleクラスのdaoがインジェクションされる
	@Autowired
	public TalentServiceImpl(TalentDao talentDao) {
		this.talentDao = talentDao;
	}

	@Override
	public List<Talent> getTalent() {
		//daoのメソッドを呼び出します
		//daoはデータベースにアクセスするだけなので、
		//その他処理があれば、serviceに書く。
		return talentDao.getTalent();
	}

	//追加
	@Override
	public void updateTalent(Talent talent) {
		
		talentDao.updateTalent(talent);
		
	}

}

コントローラーの修正

package com.example.demo.controllers;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import com.example.demo.dao.TalentDaoImpl;
import com.example.demo.entity.Talent;
import com.example.demo.service.TalentService;

@Controller
public class SampleController {
	
	//利用するサービスクラスを宣言して置く
	private TalentService talentService;
	
	@Autowired
	public SampleController(TalentService ts) {
		this.talentService = ts;
	}
	
	@GetMapping(value="/sample")
	public ModelAndView home(ModelAndView modelAndView) {
		modelAndView.setViewName("sample");
		modelAndView.addObject("sample","サンプルテキスト");

		List<Talent> list = talentService.getTalent();
		modelAndView.addObject("talents",list);
		
		return modelAndView;
	}
	
	
	@PostMapping(value="/sample")
	public ModelAndView post(@RequestParam("name")String name,ModelAndView modelAndView) {
		modelAndView.setViewName("home");
		modelAndView.addObject("name",name);
		
		//追加
		//適当にタレントを作ってアップデートを呼びます
		Talent talent = new Talent();
		talent.setId(1);
		talent.setName("bomberman");
		
		talentService.updateTalent(talent);
		
		return modelAndView;
	}
	
}

更新されています。updateメソッドが動いていることが確認できたので、いったんデータを戻しておきます。

トランザクション処理を使用する

トランザクションを確認するにあたり、まずはデバッグしてみます。
serviceの実装クラスを下記のように修正したら、ブレークポイントを貼っておきます。また実行時は虫のマークをクリックして実行します

この状態で実行して、送信ボタンを押下するとプログラムが一時停止します。
ブレークポイントを貼った箇所でプログラムが一時停止するので、その時の変数の値などを確認できます。

バグの解析では必ずこのデバッグを行うので、現場でも必須の知識です。

プログラムが一時停止している様子

この時、一時停止している状態でDBの様子を確認します。

すると既に更新されているのがわかります。
つまり、更新処理が走ったらDBの値は更新されています。

プログラムを再生ボタンを押して再開させます

次はとうとうトランザクション処理を入れてみます。
今と同じことをやりますので、違いを見てみましょう

ここで、テーブルのデータを更新前の状態に戻しておきます。そしてもう一度同じ個所にブレークポイントを入れてデバッグします

同じように送信ボタンを押して一時停止した時に、DBの状態を確認します

同じ個所でブレークポイントを貼ったのにも関わらず、レコードが更新されていません。

これがトランザクションの仕組みで、まだコミットされていないのです。
では処理を進めてコミットされたレコードが更新されています。

トランザクション処理とDBの状態

結構引っかかるんですけど、トランザクションがかかっている状態ではDBの更新が行われていないんですけど、JAVA側でSQLを発行した時に、更新された状態のデータが対象になります。

やることとしては、トランザクションがかかった状態でテーブルの更新をして、コミット前にデータを取得する処理を流すということです。

具体的に示します。

package com.example.demo.service;

import java.util.List;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import com.example.demo.dao.TalentDao;
import com.example.demo.entity.Talent;

@Service
public class TalentServiceImpl implements TalentService{
	

	
	private TalentDao talentDao;
	private PlatformTransactionManager transactionManager;
	
	
	// impleクラスのdaoがインジェクションされる
	@Autowired
	public TalentServiceImpl(TalentDao talentDao,PlatformTransactionManager tm) {
		this.talentDao = talentDao;
		this.transactionManager = tm;
		
	}

	@Override
	public List<Talent> getTalent() {
		//daoのメソッドを呼び出します
		//daoはデータベースにアクセスするだけなので、
		//その他処理があれば、serviceに書く。
		return talentDao.getTalent();
	}

	//追加
	@Override
	public void updateTalent(Talent talent) {
		
		TransactionDefinition def = new DefaultTransactionDefinition();
		TransactionStatus status = transactionManager.getTransaction(def);
		
		System.out.println("アップデートします。");
		
		talentDao.updateTalent(talent);
		
		List<Talent> current = talentDao.getTalent();
				
		
		System.out.println("アップデート完了");
		
		transactionManager.commit(status);
		
	}

}

デバッグして中身を確認します。

この時、DBを検索しても更新されていません。
では、currentの値はどうなっているでしょうか?

デバッグ中は変数の値を確認できるので確認してみます。
Variablesタブに変数一覧があります。その中でcurrent変数の中身が確認できます。

名前が更新されているのがわかります。

pgAdminの画面でSQLを発行してもコミット前なので、値は更新されていません。

Javaのプログラムの中では更新済です。
しかし実際のDBの値は更新されていません。

実行されている環境が違うように感じますね。
そのため、トランザクション処理を知らないと、バグ検証の時に実際のDBの値を確認しようとした時に、不都合が発生します。

実際の現場ではトランザクションはよく使われるので、こういった知識が無いとうまくデバッグすらできなくなるので知っておくと良いと思います。

トランザクションはアノテーションを使うやり方もあります。
今回はトランザクション処理というものを知るために、明示的な書き方で行いました。