LoginSignup
104
82

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-15

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)
104
82
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
104
82