前回、JPAのプロジェクションを使って効率的にデータの一部を取得するテクニックを学びました。これで、データの入出力に関する知識はほぼ完璧です。
今回は、業務システムの信頼性、特にデータベース操作の信頼性を保証するための非常に重要な概念である「トランザクション管理」について学びます。
トランザクションは、複数のデータベース操作を一つのまとまりとして扱い、データの一貫性と整合性を守るための仕組みです。
1. トランザクションとは何か?
**トランザクション(Transaction)とは、一連の不可分(Atomic)**なデータベース操作のことです。
例えば、「銀行口座Aから10,000円を引き出し、口座Bに10,000円を振り込む」という処理を考えます。この処理は、以下の2つのDB操作から成り立っています。
- 口座Aの残高を減らす(UPDATE)
- 口座Bの残高を増やす(UPDATE)
もし、1が成功した直後にシステム障害が発生し、2が実行されなかったらどうなるでしょうか?口座Aからはお金が減ったのに、口座Bには増えず、お金が消滅してしまいます。
トランザクションは、このような事態を防ぎ、2つの操作が必ず両方成功するか、あるいは両方失敗することを保証します。
トランザクションの4つの特性(ACID特性)
トランザクションは、以下の4つの特性(ACID特性)を満たす必要があります。
| 特性 | 意味 | 補足 |
| Atomicity(原子性) | 全ての操作が成功するか、全て失敗して元に戻る(Rollback)。 | 「全部やるか、全くやらないか」を保証。 |
| Consistency(一貫性) | トランザクションの開始前と終了後で、データの整合性が保たれる。 | 矛盾した状態にならないことを保証。 |
| Isolation(隔離性) | 複数のトランザクションが同時に実行されても、互いに影響を与えない。 | 処理は排他的に行われる(同時実行制御)。 |
| Durability(永続性) | トランザクションが成功(Commit)したら、その結果は永続的に保持される。 | DBに物理的に書き込まれることを保証。 |
2. Springにおけるトランザクション管理
従来のJava開発では、トランザクションの開始(begin())と終了(commit()/rollback())を、開発者が手動でコードに記述する必要がありました。
Spring Frameworkでは、この面倒なトランザクション制御を、宣言的に、つまりアノテーションを付けるだけで実現できます。
@Transactional アノテーション
Springでトランザクション管理を行う中心的なアノテーションが @Transactional です。
このアノテーションをService層のクラス全体、または特定のメソッドに付与します。
Java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
// Service層のクラス全体に @Transactional を付与するケースが多い
@Service
@Transactional
@RequiredArgsConstructor
public class TransferService {
private final AccountRepository accountRepository;
// このメソッド内の複数のDB操作が、一つのトランザクションとして扱われる
public void transfer(Long fromId, Long toId, int amount) {
// 1. 口座Aから引き出し
Account fromAccount = accountRepository.findById(fromId).orElseThrow();
fromAccount.setBalance(fromAccount.getBalance() - amount);
accountRepository.save(fromAccount); // DB操作①
// 【システムエラーやランタイム例外が発生した場合を想定】
if (amount > 100000) {
// RuntimeException (実行時例外)をスロー
throw new IllegalArgumentException("一回の振り込み額が上限を超えています");
}
// 2. 口座Bへ入金
Account toAccount = accountRepository.findById(toId).orElseThrow();
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(toAccount); // DB操作②
// 3. 例外が発生しなければ、自動的にコミット(Commit)される
}
// メソッドが正常終了した場合 -> コミット
// RuntimeExceptionが発生した場合 -> ロールバック(Rollback)
}
3. トランザクションの自動制御
@Transactional が付与されたメソッドが実行されると、Springは以下の制御を自動で行います。
- 開始: メソッドが呼び出される前に、トランザクションを開始します。
- 実行: メソッド内の全てのDB操作を実行します。
- コミット: メソッドが正常に終了した場合、全ての変更をDBに永続的に反映(Commit)します。
- ロールバック: メソッド実行中に
RuntimeException(実行時例外) またはErrorが発生した場合、それまでに実行された全てのDB操作を取り消し(Rollback)、データ開始前の状態に戻します。
ポイント:
@Transactionalは、デフォルトでチェック例外(Checked Exception)(例:IOException)が発生した場合はロールバックしません。ロールバックさせたい場合は、@Transactional(rollbackFor = Exception.class)のように明示的に設定する必要があります。
4. 読み取り専用トランザクション
データを変更せず、単にDBから読み取るだけのメソッド(例: findById、findAll)には、readOnly 属性を指定するのがベストプラクティスです。
Java
@Service
@Transactional(readOnly = true) // クラス全体を読み取り専用に設定
public class ItemService {
// データ変更を行うメソッドのみ、個別に上書き
@Transactional(readOnly = false) // または単に @Transactional
public Item saveItem(Item item) {
// ... 保存処理
}
// 読み取り専用のメソッド(高速化とDBリソースの節約に貢献)
// クラス設定により readOnly = true が適用される
public List<Item> findAllItems() {
// ... 検索処理
}
}
readOnly = true に設定することで、DB側でも最適化(ロックの省略など)が行われ、パフォーマンスが向上し、DBリソースの節約に繋がります。
✅ 本日のまとめ
- トランザクションは、複数のDB操作を不可分な一つの単位として扱い、ACID特性(原子性、一貫性、隔離性、永続性)を保証する。
- Springでは、
@TransactionalアノテーションをService層のメソッドに付けることで、トランザクションの開始、コミット、ロールバックを宣言的に自動管理できる。 - デフォルトでは、
RuntimeExceptionのみロールバックの対象となる。 - データの読み取りのみを行うメソッドには、
@Transactional(readOnly = true)を設定し、パフォーマンスを向上させる。
🔔 次回予告
トランザクション管理を覚えたことで、業務処理の信頼性は格段に向上しました。しかし、システムが大きくなると、同時に複数のユーザーが同じデータにアクセスする並行処理の問題が出てきます。
次回は、トランザクションの隔離レベル(Isolation Level)という概念を学び、同時実行時のデータの競合(コンフリクト)をどのように防ぐかについて深く理解します。
次回:【第27回】トランザクションの隔離レベルと並行処理 にご期待ください!


コメント