Java
spring
Transaction

Spring @Transactionalのあれこれを検証してみた

springの@Transactionalは、例外が起こった時に自動でロールバックしてくれる便利なアノテーションである。

しかし(私は)単体テストでロールバックの確認をすることがなく、本当に効くのか不安になることがある。

ということで@Transactionalの動きを色々検証してみた。

確認環境

JDK 1.8.0

Spring Boot 2.0.5.RELEASE

Doma2.0


@TransactionalはDIされたクラスから直接呼ばれるメソッドにつけないと機能しない


検証手順


  1. コントローラからDIされているサービス内のメソッドAを呼ぶ。


  2. @TransactionalがついているメソッドBをメソッドAから呼ぶ。

  3. メソッド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がつく。


検証手順


  1. サービスクラスに@Transactionalをつける

  2. メソッド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 |
+---------+-----------+----------+


コード変更

rollbackForException.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)