Springで@Transactionalを付与したメソッドを別のメソッドから呼んでみる。ロールバックされるか？されないか？

## 疑問
`@Transactional`アノテーションをつけたメソッドを同じクラス内の別のメソッドから呼び出した時に、途中で例外が発生した場合ロールバックされるのか？されないのか？気になったので調べてみました。

<details><summary>結論から言えという方はこちら</summary>
<code>@Transactional</code>アノテーションをつけたとしても、同じクラスの別のメソッドからの呼び出された場合、<code>@Transactional</code>アノテーションがない場合と全く同じ挙動になる。
</details>


## 手を動かしてみる

まず、公式チュートリアルからコードを拝借してきましょう。
チュートリアル完了後のコードをVSCodeで開いてみます。

```
# コードをクローンしてくる
git clone https://github.com/spring-guides/gs-managing-transactions.git
# VSCodeでプロジェクトを開く
code ./gs-managing-transactions/complete
```

使用されているテーブルの定義はとてもシンプルで、以下のようになっています。

```sql: src/main/resources/schema.sql
drop table BOOKINGS if exists;
create table BOOKINGS(ID serial, FIRST_NAME varchar(5) NOT NULL);
```

初めに定義されているサービスクラスは以下のようになっています。
これを改造して遊んでいきましょう。


```java: src/main/java/com/example/managingtransactions/BookingService.java
package com.example.managingtransactions;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class BookingService {

	private final static Logger logger = LoggerFactory.getLogger(BookingService.class);

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional
	public void book(String... persons) {
		for (String person : persons) {
			logger.info("Booking " + person + " in a seat...");
			jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
		}
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

試しに一番簡単なものに書き直してみましょう。
`insertBooking1()`メソッドで1つの`FIRST_NAME`カラムの値が`"one"`のレコードを登録して、`findAllBookings()`メソッドで全てのレコードの`FIRST_NAME`カラムの値を`List<String>`の形式で取得しています。


```java
@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional
	public void insertBooking1(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

これを呼び出す`AppRunner`クラスも修正して以下のようにします。

```java: src/main/java/com/example/managingtransactions/AppRunner.java
package com.example.managingtransactions;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

@Component
class AppRunner implements CommandLineRunner {

	private final BookingService bookingService;

	public AppRunner(BookingService bookingService) {
		this.bookingService = bookingService;
	}

	@Override
	public void run(String... args) throws Exception {
		bookingService.insertBooking1();
		Assert.isTrue(bookingService.findAllBookings().size()==1);
	}
}
```

では、ここで一度実行してみましょう。データベースの準備をしていませんが、心配しなくて大丈夫です。インメモリーデータベースのH2 Databaseがデフォルトで使用されるようになっています。以下のコマンドで実行してみてエラーが出なければ成功です。

```
./gradlew bootRun
```

どんなエラーが出るのか確認してみたければ、上記のコードの`bookingService.findAllBookings().size()==1`の`1`を他の数字に変えてみるといいでしょう。(あまりわかりやすいエラーではないですね。)

これで`bookingService.insertBooking1()`でデータベースに1つのレコードが登録されることが確認できました。

では`BookingService`の他のメソッドから`insertBooking1`を呼び出してみたいと思います。
`BookingService`クラスを以下のように変更してください。
`insertMultipleBookings()`メソッドが2つのレコードを登録するようになっています。

```java
@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional
	public void insertBooking1(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
	}
	@Transactional
	public void insertBooking2(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "two");
	}

	/**
	 * 複数のBookingを登録する
	 */
	@Transactional
	public void insertMultipleBookings(){
		insertBooking1();
		insertBooking2();
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

続いて`AppRunner`クラスを変更して`insertMultipleBookings()`メソッドを呼び出すようにしてみましょう。

```java
@Component
class AppRunner implements CommandLineRunner {

	private final BookingService bookingService;

	public AppRunner(BookingService bookingService) {
		this.bookingService = bookingService;
	}

	@Override
	public void run(String... args) throws Exception {
		bookingService.insertMultipleBookings();
		Assert.isTrue(bookingService.findAllBookings().size()==2);
	}
}
```

この状態で先ほどと同じく`./gradlew bootRun`を実行すると成功することが確認できます。
ここでやっと本題の`insertMultipleBookings()`メソッドの途中でエラーが発生した時にロールバックされるか？されないのか？確かめていきたいと思います。

`insertMultipleBookings()`メソッドを以下のように変更します。

```java
    @Transactional
	public void insertMultipleBookings(){
		insertBooking1();
		insertBooking2();
		throw new RuntimeException("ロールバックしてほしいエラー");
	}
```

`AppRunner`クラスの`run()`メソッドも以下のように修正します。

```java
	@Override
	public void run(String... args) throws Exception {
		try {
			bookingService.insertMultipleBookings();
		} catch (Exception e) {
			// TODO: handle exception
		}
		Assert.isTrue(bookingService.findAllBookings().size()==0);
	}
```

さて、ロールバックされているでしょうか。`./gradlew bootRun`を実行してみましょう。どうやらロールバックされているようですね。
巷で「同じクラスのメソッドから呼び出されると`@Transactional`をつけていてもロールバックされない」というような話を風の噂で聞いたような気がしなくもないのですが、そんなことはないようです。
ちなにみ、今の`insertBooking1()`メソッド`insertBooking2()`メソッド`@Transactional`をつけてなくてもロールバックされます。
試してみましょう。

```java
@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}


	public void insertBooking1(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
	}
	
	public void insertBooking2(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "two");
	}

	/**
	 * 複数のBookingを登録する
	 */
	@Transactional
	public void insertMultipleBookings(){
		insertBooking1();
		insertBooking2();
		throw new RuntimeException("ロールバックしてほしいエラー");
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

まあ、これは以下のように書いた場合と全く同じなので、当然といっちゃ当然です。


```java
@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	/**
	 * 複数のBookingを登録する
	 */
	@Transactional
	public void insertMultipleBookings(){
        jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
        jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "two");
		throw new RuntimeException("ロールバックしてほしいエラー");
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

Spring frameworkは`@Transactional`がついたメソッドが実行された時に内部で何やかんやしてる([詳しくはこの辺を参照するとわかった気になれるかも](https://spring.pleiades.io/spring-framework/docs/current/reference/html/core.html#aop-understanding-aop-proxies))からです。要するに同じクラス内のメソッドから呼び出されたときは`@Transactional`メソッドは何の意味もないということです。
これは寧ろ好ましいことだと思います。同じサービスクラス内で何度も呼び出されるようなメソッドは普通トランザクションを分離させないのではないでしょうか。他のSQLと同じタイミングでロールバックさせたいのではないでしょうか。もし、そうでないなら他のサービスに分けた方がいいのではと思います(強い主張)。そういった意味で、同じクラス内のメソッドから呼び出された際に普通のメソッドのように振る舞うのは良いことだと思えます。
混乱を避けるために呼び出される側のメソッド(今回の場合は`insertBooking1()`と`insertBooking2()`)からは`@Transactional`アノテーションを外しましょう。

そうはいっても別トランザクションで実行したいんだということもあるかと思います。
その場合は上述のように別のクラスで実装してDIされたインスタンスを呼び出すしかないです。

試しに`insertBooking1()`メソッドに`@Transactional(propagation=Propagation.REQUIRES_NEW)`をつけてみましょう。(`insertBooking2()`メソッドにも`@Transactional`を戻しています。)
このコードを読んだなら「`insertMultipleBookings()`メソッドが実行されたら別トランザクションで実行されている`insertBooking1()`だけがコミットされてレコードが一件登録されるだろうな。」という気持ちになるかと思います(でも、そうはならなかった)。

```java
@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void insertBooking1(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
	}
	@Transactional
	public void insertBooking2(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "two");
	}

	/**
	 * 複数のBookingを登録する
	 */
	@Transactional
	public void insertMultipleBookings(){
		insertBooking1();
		insertBooking2();
		throw new RuntimeException("ロールバックしてほしいエラー");
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

`AppRunner`の方も先程の予想に従って変更してみましょう。

```java
@Component
class AppRunner implements CommandLineRunner {

	private final BookingService bookingService;

	public AppRunner(BookingService bookingService) {
		this.bookingService = bookingService;
	}

	@Override
	public void run(String... args) throws Exception {
		try {
			bookingService.insertMultipleBookings();
		} catch (Exception e) {
			// TODO: handle exception
		}
		Assert.isTrue(bookingService.findAllBookings().size()==1);
	}
}
```

実行してみるとエラーになるのが確認できますね。

では、別のクラスに`insertBooking1()`メソッドと`insertBooking2()`メソッドを移動させてみましょう。新しく`OtherService`クラスを作ります。

```java: src/main/java/com/example/managingtransactions/OtherService.java
package com.example.managingtransactions;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

@Component
public class OtherService {

	private final JdbcTemplate jdbcTemplate;

	public OtherService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void insertBooking1(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "one");
	}
	@Transactional
	public void insertBooking2(){
		jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", "two");
	}

}
```

`BookingService`クラスからも新しく作った`OtherService`クラスを呼び出すように変更してみます。

```java
package com.example.managingtransactions;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

@Component
public class BookingService {

	private final JdbcTemplate jdbcTemplate;
	private final OtherService otherService;

	public BookingService(JdbcTemplate jdbcTemplate,OtherService otherService) {
		this.jdbcTemplate = jdbcTemplate;
		this.otherService = otherService;
	}

	/**
	 * 複数のBookingを登録する
	 */
	@Transactional
	public void insertMultipleBookings(){
		otherService.insertBooking1();
		otherService.insertBooking2();
		throw new RuntimeException("ロールバックしてほしいエラー");
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
```

先程の`insertBooking1()`メソッドのみをコミットさせたいという予想を叶えるための修正なので`AppRunner`クラスには修正を加える必要はありません。では、改めて`./gradlew bootRun`を実行してみましょう。
今度はエラーにないはずです。

## まとめ

**結局何が言いたかったか**というと「`@Transactional`アノテーションをつけたとしても、同じクラスの別のメソッドからの呼び出された場合、`@Transactional`アノテーションがない場合と全く同じ挙動になる。」ということです。

まあ、この挙動は`assertj`モードというのを使えば変えられるのですが、あまり使うことはないかと思うので詳細は割愛します(私も詳しくはしらないので)。

## 感想

実際に手を動かして実験するのは大切だな〜〜〜と染み染み。。。
「しらんけど、たぶん、こんな仕様やろ。」となっていた昨日より、だいぶ理解が深まった気がします。