springの@Transactional
は、例外が起こった時に自動でロールバックしてくれる便利なアノテーションである。
しかし(私は)単体テストでロールバックの確認をすることがなく、本当に効くのか不安になることがある。
ということで@Transactional
の動きを色々検証してみた。
確認環境
JDK 1.8.0
Spring Boot 2.0.5.RELEASE
Doma2.0
@Transactional
はDIされたクラスから直接呼ばれるメソッドにつけないと機能しない
検証手順
- コントローラからDIされているサービス内のメソッドAを呼ぶ。
-
@Transactional
がついているメソッドBをメソッドAから呼ぶ。 - メソッドBでDBにレコード挿入した直後に例外発生させる。
検証コード
コントローラ
public class TestController {
@Autowired
TestService testService;
@PostMapping("/post")
public void postTest() {
testService.insertMethodA();
}
}
サービス
@Service
public class TestService {
@Autowired
ItemDao itemDao;
public void insertMethodA() {
insertMethodB();
}
@Transactional
public void insertMethodB() {
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
// 例外発生
throw new RuntimeException();
}
}
実行結果
ロールバックされず、挿入されちゃっている。
mysql> select * from item;
+---------+-----------+----------+
| ITEM_CD | ITEM_NAME | SELL_FLG |
+---------+-----------+----------+
| 1 | hoge | 0 |
+---------+-----------+----------+
コード修正
@Transactional
をメソッドAにつける。
@Transactional
public void insertMethodA() {
insertMethodB();
}
public void insertMethodB() {
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
// 例外発生
throw new RuntimeException();
}
実行結果
mysql> select * from item;
Empty set (0.00 sec)
ロールバックされました。はいおっけ~。
@Transactinal
をクラスにつけるとクラス内の全メソッドにつく
クラスに@Transactional
をつけると、暗黙的にクラス内の全メソッドに@Transactional
がつく。
検証手順
- サービスクラスに
@Transactional
をつける - メソッドAに
@Transactional
をつけずコントローラから呼び出し
検証コード
コントローラは先述のものと同様(サービスクラスからメソッドAを呼ぶ)。
サービス
@Service
@Transactional
public class TestService {
@Autowired
ItemDao itemDao;
public void insertMethodA() {
insertMethodB();
}
public void insertMethodB() {
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
// 例外発生
throw new RuntimeException();
}
}
実行結果
ロールバックされてます。
コントローラからメソッドBを呼んだ場合もロールバックされました。
mysql> select * from item;
Empty set (0.00 sec)
readonly
属性をtrue
にしてレコード操作しようとすると例外が投げられる
@Transactinal
にはreadonly
という属性が用意されており、true
に設定するとレコード操作処理が走った際に例外を投げてくれます。
検証コード
コントローラは先述のものを流用。
サービス
@Service
public class TestService {
@Autowired
ItemDao itemDao;
@Transactional(readOnly = true)
public void insertMethodA() {
insertMethodB();
}
public void insertMethodB() {
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
}
}
実行結果
java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
更新を想定していない取得系のサービスとかに設定するのがよいですね~。
rollbackFor
属性でロールバック対象の発生例外を設定できる
デフォルトではロールバック対象の発生例外はRuntimeException
とそのサブクラスになっています。rollbackFor
属性は、値に設定した例外クラスとそのサブクラスをロールバック対象の発生例外に変更します。
検証コード
rollbackFor
をつけないデフォルトの状態で、Exception
例外を発生させる。
@Service
public class TestService {
@Autowired
ItemDao itemDao;
@Transactional
public void insertMethodA() throws Exception{
insertMethodB();
}
public void insertMethodB() throws Exception{
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
// 例外発生
throw new Exception();
}
}
実行結果
ロールバックされません。
mysql> select * from item;
+---------+-----------+----------+
| ITEM_CD | ITEM_NAME | SELL_FLG |
+---------+-----------+----------+
| 1 | hoge | 0 |
+---------+-----------+----------+
コード変更
rollbackFor
にException.class
を設定します。
@Transactional(rollbackFor = Exception.class)
public void insertMethodA() throws Exception{
insertMethodB();
}
実行結果(変更後)
ロールバックされるようになりました。
mysql> select * from item;
Empty set (0.00 sec)
外部クラスにつけた@Transactional
は内部クラスに適用されない
外部クラスにつけた@Transactional
は内部クラスには適用されないので、別途内部クラスにつける必要がある。
Junitで内部クラスを使ってテストクラスを階層化している場合などは注意。
検証コード
コントローラ
public class TestController {
@Autowired
TestService testService;
@Autowired
TestService.TestInnerService testInnerService;
@PostMapping("/post")
public void postTest() throws Exception{
testInnerService.insertMethodA();
}
}
サービス
@Service
@Transactional
public class TestService {
@Autowired
ItemDao itemDao;
@Service
public class TestInnerService{
public void insertMethodA() throws Exception{
insertMethodB();
}
public void insertMethodB(){
// 挿入レコード用意
Item item = new Item();
item.setItemCd("1");
item.setItemName("hoge");
item.setSellFlg("0");
// レコード挿入
itemDao.insert(item);
// 例外発生
throw new RuntimeException();
}
}
}
実行結果
ロールバックされていない。
mysql> select * from item;
+---------+-----------+----------+
| ITEM_CD | ITEM_NAME | SELL_FLG |
+---------+-----------+----------+
| 1 | hoge | 0 |
+---------+-----------+----------+
コード変更
内部クラスに@Transactinal
をつける。
@Service
@Transactional
public class TestInnerService{
実行結果(変更後)
ロールバックされました。
mysql> select * from item;
Empty set (0.00 sec)