SpringBootで@Transactonal
を使用する際に、どのような場合にロールバックが発生するか、および例外処理の種類について学習したので、ここに記録する。
@Transactional
の定義
Springドキュメントで@Transactional
の定義について調べると、概ね以下の通り。
アノテーションインターフェース Transactional
・このアノテーションがクラスレベルで宣言されると、宣言しているクラスとそのサブクラスのすべてのメソッドにデフォルトとして適用されます。
・このアノテーションでカスタムロールバックルールが構成されていない場合、トランザクションは RuntimeExceptionSE および ErrorSE でロールバックされますが、チェックされた例外ではロールバックされません。
要約すると、@Transactional
を宣言したクラス(P-Classと呼ぶ)およびそのサブクラス(C-Classes)でRuntimeExceptionおよびErrorが発生した場合にP-Classの処理がロールバックされるということになる。
実例での解説
下記は受講生登録システムとして、受講生情報と受講生コース情報を一括で登録するサービスメソッドである。処理の流れは以下の通り
- 受講生情報を登録
- 受講生コース情報を登録
1.存在しない受講生idまたはコースidの場合はResourceNotFoundExceptionが発生
クリックしてコードを表示
@Service
public class StudentService {
@Transactional
public void registerStudent(StudentDetail studentDetail) throws ResourceNotFoundException {
// ①受講生情報を登録
Student student = studentDetail.getStudent();
repository.insertStudent(student);
// ②コース情報を登録
StudentCourse studentCourse = studentDetail.getStudentCourses().get(0);
studentCourse.setStudentId(student.getId());
registerStudentCourse(studentCourse);
}
@Transactional
public void registerStudentCourse(StudentCourse studentCourse) throws ResourceNotFoundException {
// courseIdが存在するかを確認
searchCourseNameById(studentCourse.getCourseId());
// studentIdが存在するかを確認
searchStudentById(studentCourse.getStudentId());
// 存在する場合は登録(同時に申し込み状況を新規登録)
repository.insertStudentCourse(studentCourse);
repository.insertStudentCourseStatus(new StudentCourseStatus(studentCourse.getId()));
}
public String searchCourseNameById(int id) throws ResourceNotFoundException {
return repository.searchCourseNameById(id)
.orElseThrow(() -> new ResourceNotFoundException("指定されたIDのコースは存在しません"));
}
public Student searchStudentById(int id) throws ResourceNotFoundException {
return repository.searchStudentById(id)
.orElseThrow(() -> new ResourceNotFoundException("指定されたIDの受講生は存在しません"));
}
}
ここで、下記のようなjsonで登録を試みる。"courseId": 100は、存在しないものであり、searchCourseNameByIdメソッドにおいてResouceNotFoundExceptionを発生させるためのトリガーである。
{
"name": "ロールバックなし",
"kanaName": "ロールバックナシ",
"nickname": "ナシ",
"email": "nashi@example.com",
"livingArea": "東京都渋谷区",
"age": 30,
"gender": "男性",
"remark": "",
"courseId": 100
}
このjsonを用いてregisterStudentサービスメソッドを動かすと下記の通り。
ちなみにPostman上では「指定されたIDのコースは存在しません」が表示され、ResouceNotFoundExceptionが発生したのを確認している。
・登録前
mysql> SELECT id, name FROM students;
+----+-----------------------------+
| id | name |
+----+-----------------------------+
| 1 | 山田太郎 |
//省略
| 64 | 鈴木 一郎 |
+----+-----------------------------+
・登録後
mysql> SELECT id, name FROM students;
+----+-----------------------------+
| id | name |
+----+-----------------------------+
| 1 | 山田太郎 |
//省略
| 64 | 鈴木 一郎 |
| 65 | ロールバックなし |
+----+-----------------------------+
@Transactional
が効いておらず、受講生が登録されてしまった。
冒頭で記載した通り、@Transactional
はRuntimeExceptionおよびErrorでロールバックされるのであり、Exceptionを継承したResouceNotFoundExceptionではロールバックは無効となる。よって、ResouceNotFoundException以前に実行された受講生登録はロールバックされない。
これの対策としては、カスタムルールの設定が必要。
@Transactionalの後ろに(rollbackFor = ResourceNotFoundException.class)
を付加する。これにより、ResourceNotFoundExceptionによってもロールバックが発生するようになる。
@Service
public class StudentService {
@Transactional(rollbackFor = ResourceNotFoundException.class)
public void registerStudent(StudentDetail studentDetail) throws ResourceNotFoundException {
// ①受講生情報を登録
Student student = studentDetail.getStudent();
repository.insertStudent(student);
// ②コース情報を登録
StudentCourse studentCourse = studentDetail.getStudentCourses().get(0);
studentCourse.setStudentId(student.getId());
registerStudentCourse(studentCourse);
}
}
なお、ドキュメントの記載の通り@Transactional
はクラスに付加するものであり、各々のメソッドに付加するのは冗長かもしれない。クラスやメソッドの設計によるところと思う。
RuntimeExceptiionとException
巷で解説記事を多く見かけるが、そもそものこの2つの分類についてピンと来ない。例えば、上記の実例でのResouceNotFoundExceptionはExceptionを継承しているが、機能上はRuntimeExceptionに置き換えても、適切に例外がthrowされてさえいればおそらく問題ない。
具体的なクラスを挙げれば、ArithmeticExceptionは非検査例外(RuntimeException)であり、必ずしも例外をthrowする必要はない。ただし、ユーザーから入力を受け付けて計算するようなアプリでは0除算が発生した際に、例外処理がされていない場合にはユーザー側に何が起こっているかわからず、使用感のいいアプリとは言えない。
結局のところ、予想されるエラー種別を適切に把握してその対応を考えるということがアプリ開発では必要であり、そこにExceptionやRuntimeExceptionを区別するのはあまり意味がないというのが自分なりの結論である。