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にレコード挿入した直後に例外発生させる。

### 検証コード

**コントローラ**

```java

public class TestController {

    @Autowired
    TestService testService;

    @PostMapping("/post")
    public void postTest() {
        testService.insertMethodA();
    }
}
```
**サービス**

```java

@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につける。

```java

    @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を呼ぶ）。


**サービス**

```java

@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`に設定するとレコード操作処理が走った際に例外を投げてくれます。

### 検証コード
コントローラは先述のものを流用。

**サービス**

```java

@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`例外を発生させる。

```java
@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`を設定します。

```java

    @Transactional(rollbackFor = Exception.class)
    public void insertMethodA() throws Exception{
        insertMethodB();
    }
```

### 実行結果（変更後）
ロールバックされるようになりました。

```
mysql> select * from item;
Empty set (0.00 sec)
```
# 外部クラスにつけた`@Transactional`は内部クラスに適用されない
外部クラスにつけた`@Transactional`は内部クラスには適用されないので、別途内部クラスにつける必要がある。
Junitで内部クラスを使ってテストクラスを階層化している場合などは注意。
### 検証コード

**コントローラ**

```java
public class TestController {

    @Autowired
    TestService testService;

    @Autowired
    TestService.TestInnerService testInnerService;

    @PostMapping("/post")
    public void postTest() throws Exception{
       testInnerService.insertMethodA();
    }
}
```

**サービス**

```java

@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`をつける。

```java

    @Service
    @Transactional
    public class TestInnerService{
```

### 実行結果（変更後）
ロールバックされました。

```
mysql> select * from item;
Empty set (0.00 sec)
```