Springの@Transactionalアノテーションは、宣言的トランザクション管理を実現する便利な機能です。しかし、この便利さの裏には「効いているつもりが効いていない」という厄介な落とし穴が存在します。特に、自己呼び出し問題(同一クラス内での@Transactionalメソッド呼び出し)は、多くの開発者が遭遇するトラブルの代表格です。本記事では、Spring @Transactionalが効かない典型パターン、Springのプロキシベースのトランザクション管理の仕組み、そして自己呼び出し問題の具体的な解決策を解説します。トランザクション管理の内部動作を理解することで、本番環境での予期せぬデータ不整合を未然に防ぎましょう。
実行環境と前提条件#
本記事のサンプルコードは、以下の環境で動作確認しています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| Spring Data JPA |
3.4.x(Spring Boot Starterに含まれる) |
| Hibernate |
6.6.x(Spring Data JPAに含まれる) |
| データベース |
PostgreSQL 16.x または H2 Database(開発用) |
| ビルドツール |
Maven または Gradle |
事前に以下の準備を完了してください。
- JDK 17以上のインストール
- Spring Boot + Spring Data JPAプロジェクトの基本構成
spring-boot-starter-data-jpa依存関係の追加
Springのプロキシベーストランザクション管理の仕組み#
@Transactionalが効かないパターンを理解するには、まずSpringがどのようにトランザクション管理を実現しているかを把握する必要があります。
プロキシパターンによるAOP実装#
Springの@Transactionalは、AOP(Aspect-Oriented Programming)の仕組みを利用して実装されています。具体的には、@Transactionalが付与されたBeanに対してプロキシオブジェクトを生成し、このプロキシがトランザクションの開始・コミット・ロールバックを制御します。
sequenceDiagram
participant Client as 呼び出し元
participant Proxy as プロキシ<br>(トランザクション制御)
participant Target as 実際のBean
Client->>Proxy: メソッド呼び出し
Note over Proxy: トランザクション開始
Proxy->>Target: 実メソッド実行
Target-->>Proxy: 戻り値
alt 正常終了
Note over Proxy: コミット
else 例外発生
Note over Proxy: ロールバック
end
Proxy-->>Client: 戻り値プロキシの種類#
Springは2種類のプロキシ方式をサポートしています。
| プロキシ方式 |
条件 |
特徴 |
| JDKダイナミックプロキシ |
インターフェースを実装している場合 |
インターフェースのメソッドのみプロキシ可能 |
| CGLIBプロキシ |
インターフェースを実装していない場合 |
クラスを継承してプロキシを生成 |
Spring Boot 2.0以降では、デフォルトでCGLIBプロキシが使用されます。これはspring.aop.proxy-target-class=trueがデフォルト設定となっているためです。
flowchart TD
A[Spring Bootアプリケーション起動] --> B{Beanに@Transactional<br>が付与されている?}
B -->|Yes| C{インターフェースを<br>実装している?}
B -->|No| D[通常のBeanとして登録]
C -->|Yes| E[JDKダイナミックプロキシ生成]
C -->|No| F[CGLIBプロキシ生成]
E --> G[DIコンテナにプロキシを登録]
F --> Gプロキシが介在しない呼び出し#
重要なポイントは、プロキシを経由しない呼び出しではトランザクション制御が行われないということです。これが@Transactionalが効かない問題の根本原因です。
@Transactionalが効かない典型パターン#
実務で遭遇しやすい「@Transactionalが効かない」パターンを見ていきましょう。
パターン1: 自己呼び出し(同一クラス内での呼び出し)#
最も頻繁に遭遇する問題が自己呼び出しです。同一クラス内で@Transactionalメソッドを呼び出すと、プロキシを経由せずに直接メソッドが実行されるため、トランザクションが適用されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// 外部から呼び出されるメソッド
public void processOrder(Long orderId) {
// ビジネスロジック...
// 自己呼び出し - @Transactionalが効かない!
this.updateOrderStatus(orderId, OrderStatus.PROCESSING);
}
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus status) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus(status);
orderRepository.save(order);
// 例外が発生してもロールバックされない
if (status == OrderStatus.FAILED) {
throw new OrderProcessingException("注文処理に失敗しました");
}
}
}
|
この問題が発生する理由を図で示します。
sequenceDiagram
participant Client as Controller
participant Proxy as OrderServiceプロキシ
participant Target as OrderService実体
Client->>Proxy: processOrder()
Note over Proxy: トランザクション開始なし<br>(@Transactionalなし)
Proxy->>Target: processOrder()
Target->>Target: this.updateOrderStatus()<br>【直接呼び出し】
Note over Target: プロキシを経由していない<br>トランザクション開始されない
Target-->>Proxy: 戻り値
Proxy-->>Client: 戻り値this.updateOrderStatus()は、プロキシではなく実体のメソッドを直接呼び出しているため、@Transactionalのインターセプトが発生しません。
パターン2: privateメソッドへの@Transactional#
privateメソッドに@Transactionalを付与しても、トランザクションは適用されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
public void processPayment(PaymentRequest request) {
// 何らかの前処理...
executePayment(request);
}
// privateメソッド - @Transactionalが効かない!
@Transactional
private void executePayment(PaymentRequest request) {
Payment payment = new Payment(request);
paymentRepository.save(payment);
// 例外が発生してもロールバックされない
externalPaymentGateway.charge(request);
}
}
|
理由: CGLIBプロキシはクラスを継承してプロキシを生成しますが、privateメソッドはオーバーライドできないため、プロキシがインターセプトできません。
パターン3: 非Spring管理Bean(newでインスタンス化)#
Springコンテナが管理していないオブジェクトでは、@Transactionalは動作しません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// 非Spring管理 - @Transactionalが効かない!
public class ManualOrderProcessor {
private final OrderRepository orderRepository;
public ManualOrderProcessor(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void process(Order order) {
orderRepository.save(order);
// トランザクションは適用されない
}
}
@Service
public class OrderController {
private final OrderRepository orderRepository;
public OrderController(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void handleOrder(Order order) {
// newでインスタンス化 - Springの管理外
ManualOrderProcessor processor = new ManualOrderProcessor(orderRepository);
processor.process(order); // トランザクションなし
}
}
|
理由: newでインスタンス化されたオブジェクトはSpring DIコンテナの管理外であり、プロキシが生成されません。
パターン4: finalクラス・finalメソッドへの@Transactional#
finalクラスやfinalメソッドに@Transactionalを付与しても効果がありません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// finalクラス - CGLIBがサブクラスを作成できない
@Service
public final class FinalOrderService {
@Transactional
public void createOrder(Order order) {
// トランザクションが効かない可能性あり
}
}
@Service
public class OrderService {
// finalメソッド - オーバーライドできない
@Transactional
public final void createOrder(Order order) {
// トランザクションが効かない
}
}
|
理由: CGLIBはターゲットクラスを継承してプロキシを生成しますが、finalクラスは継承できず、finalメソッドはオーバーライドできません。
自己呼び出し問題の解決策#
最も頻出する自己呼び出し問題に対する解決策を、推奨度順に紹介します。
解決策1: コンポーネントの分離(推奨)#
最もシンプルで推奨される方法は、トランザクションが必要な処理を別のコンポーネントに分離することです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// トランザクション処理を担当する専用サービス
@Service
public class OrderStatusUpdater {
private final OrderRepository orderRepository;
public OrderStatusUpdater(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void updateStatus(Long orderId, OrderStatus status) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus(status);
orderRepository.save(order);
if (status == OrderStatus.FAILED) {
throw new OrderProcessingException("注文処理に失敗しました");
}
}
}
// メインのサービスクラス
@Service
public class OrderService {
private final OrderStatusUpdater orderStatusUpdater;
public OrderService(OrderStatusUpdater orderStatusUpdater) {
this.orderStatusUpdater = orderStatusUpdater;
}
public void processOrder(Long orderId) {
// ビジネスロジック...
// 別コンポーネント経由 - プロキシを通るためトランザクションが効く
orderStatusUpdater.updateStatus(orderId, OrderStatus.PROCESSING);
}
}
|
sequenceDiagram
participant Client as Controller
participant ServiceProxy as OrderServiceプロキシ
participant Service as OrderService実体
participant UpdaterProxy as OrderStatusUpdaterプロキシ
participant Updater as OrderStatusUpdater実体
Client->>ServiceProxy: processOrder()
ServiceProxy->>Service: processOrder()
Service->>UpdaterProxy: updateStatus()
Note over UpdaterProxy: トランザクション開始
UpdaterProxy->>Updater: updateStatus()
Updater-->>UpdaterProxy: 戻り値
Note over UpdaterProxy: コミット
UpdaterProxy-->>Service: 戻り値
Service-->>ServiceProxy: 戻り値
ServiceProxy-->>Client: 戻り値メリット:
- 単一責任の原則に沿った設計になる
- テストが容易になる
- コードの意図が明確になる
解決策2: ApplicationContextからの自己参照取得#
アーキテクチャ上、コンポーネントを分離できない場合は、ApplicationContextから自分自身のプロキシを取得する方法があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Service
public class OrderService implements ApplicationContextAware {
private final OrderRepository orderRepository;
private ApplicationContext applicationContext;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void processOrder(Long orderId) {
// ApplicationContextから自分自身のプロキシを取得
OrderService self = applicationContext.getBean(OrderService.class);
// プロキシ経由での呼び出し - トランザクションが効く
self.updateOrderStatus(orderId, OrderStatus.PROCESSING);
}
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus status) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus(status);
orderRepository.save(order);
}
}
|
より簡潔に書く場合は、@Lazyを使った自己注入も可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderService self; // 自己参照
public OrderService(OrderRepository orderRepository,
@Lazy OrderService self) {
this.orderRepository = orderRepository;
this.self = self;
}
public void processOrder(Long orderId) {
// プロキシ経由での呼び出し
self.updateOrderStatus(orderId, OrderStatus.PROCESSING);
}
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus status) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus(status);
orderRepository.save(order);
}
}
|
注意点:
- 循環依存を避けるため
@Lazyが必要
- コードの可読性が低下する可能性がある
- 設計の問題を覆い隠してしまう恐れがある
解決策3: AspectJによるLTW/CTW(上級者向け)#
プロキシベースではなく、バイトコード織り込み(Weaving)を使用することで、自己呼び出しでも@Transactionalを機能させることができます。
1
2
3
4
5
6
|
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework:spring-aspects'
implementation 'org.aspectj:aspectjweaver:1.9.22'
}
|
1
2
3
4
5
6
|
// 設定クラス
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactionConfig {
// AspectJモードでトランザクション管理を有効化
}
|
1
2
|
# application.properties
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
|
AspectJを使用する場合、JVMの起動時に-javaagentオプションでAspectJエージェントを指定する必要があります。
1
|
java -javaagent:path/to/aspectjweaver.jar -jar myapp.jar
|
メリット:
- 自己呼び出しでも
@Transactionalが機能する
privateメソッドにも適用可能
デメリット:
- 設定が複雑
- デバッグが困難になる場合がある
- ビルド時間が増加する可能性がある
チェック例外でのロールバック設定#
@Transactionalのデフォルト動作では、非チェック例外(RuntimeException)のみロールバックされます。チェック例外(Exception)が発生した場合は、デフォルトではロールバックされません。
デフォルト動作の確認#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void createOrder(OrderRequest request) throws OrderValidationException {
Order order = new Order(request);
orderRepository.save(order);
// チェック例外 - デフォルトではロールバックされない
if (!order.isValid()) {
throw new OrderValidationException("注文内容が不正です");
}
}
}
// チェック例外
public class OrderValidationException extends Exception {
public OrderValidationException(String message) {
super(message);
}
}
|
チェック例外でもロールバックする設定#
rollbackFor属性を使用して、特定の例外でロールバックするように設定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// チェック例外でもロールバックする
@Transactional(rollbackFor = OrderValidationException.class)
public void createOrder(OrderRequest request) throws OrderValidationException {
Order order = new Order(request);
orderRepository.save(order);
if (!order.isValid()) {
throw new OrderValidationException("注文内容が不正です");
}
}
// すべての例外でロールバックする
@Transactional(rollbackFor = Exception.class)
public void createOrderStrict(OrderRequest request) throws Exception {
Order order = new Order(request);
orderRepository.save(order);
externalValidationService.validate(order); // チェック例外をスローする可能性
}
}
|
ロールバック設定の一覧#
| 属性 |
説明 |
使用例 |
rollbackFor |
指定した例外クラスでロールバック |
@Transactional(rollbackFor = Exception.class) |
rollbackForClassName |
例外クラス名(文字列)でロールバック |
@Transactional(rollbackForClassName = "java.io.IOException") |
noRollbackFor |
指定した例外クラスではロールバックしない |
@Transactional(noRollbackFor = BusinessException.class) |
noRollbackForClassName |
例外クラス名(文字列)でロールバックしない |
@Transactional(noRollbackForClassName = "...") |
複数の例外を指定する#
1
2
3
4
5
6
7
8
|
@Transactional(rollbackFor = {
OrderValidationException.class,
PaymentException.class,
InventoryException.class
})
public void processOrder(OrderRequest request) throws Exception {
// 複数のチェック例外が発生する可能性がある処理
}
|
よくある誤解とアンチパターン#
誤解1: @Transactionalを付ければ必ずトランザクションが効く#
前述の通り、プロキシを経由しない呼び出しでは@Transactionalは効きません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// アンチパターン
@Service
public class BadService {
public void publicMethod() {
// 自己呼び出し - トランザクションが効かない
internalMethod();
}
@Transactional
public void internalMethod() {
// ...
}
}
|
誤解2: 例外をcatchすればロールバックを防げる#
例外が@Transactionalメソッドの外に伝播しなければロールバックは発生しません。しかし、これは意図しないデータ不整合を招く可能性があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// アンチパターン - 例外を握りつぶしてロールバックを防ぐ
@Transactional
public void riskyMethod() {
try {
// 例外が発生する可能性のある処理
repository.save(entity);
externalService.call(); // 例外発生
} catch (Exception e) {
// ログだけ出して握りつぶし - 危険!
log.error("エラー発生", e);
}
// saveは成功、externalServiceは失敗という不整合状態になる可能性
}
|
誤解3: readOnly = trueならSELECTしか実行できない#
readOnly = trueはヒントであり、強制ではありません。Hibernate/JPAはこのヒントを使ってフラッシュモードを最適化しますが、INSERT/UPDATE/DELETEの実行を完全には防止しません。
1
2
3
4
5
|
@Transactional(readOnly = true)
public void readOnlyMethod() {
Entity entity = repository.findById(1L).orElseThrow();
entity.setName("変更"); // ダーティチェックにより更新される可能性あり(実装依存)
}
|
誤解4: 非同期メソッドでも@Transactionalが効く#
@Asyncと@Transactionalを組み合わせる場合、実行スレッドが異なるため注意が必要です。
1
2
3
4
5
6
7
8
9
10
11
|
// 注意が必要なパターン
@Service
public class AsyncService {
@Async
@Transactional
public void asyncMethod() {
// 非同期で実行される
// トランザクションは新しいスレッドで開始される
}
}
|
@Transactional問題の検出方法#
1. ログレベルの設定#
トランザクションの動作を確認するため、ログレベルを設定します。
1
2
3
|
# application.properties
logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG
|
2. TransactionSynchronizationManagerの活用#
プログラムからトランザクション状態を確認できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Service
public class TransactionDebugService {
public void checkTransactionStatus() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
String txName = TransactionSynchronizationManager.getCurrentTransactionName();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
System.out.println("Transaction Active: " + isActive);
System.out.println("Transaction Name: " + txName);
System.out.println("ReadOnly: " + isReadOnly);
}
}
|
3. テストでの検証#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@SpringBootTest
@Transactional
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
void トランザクションがロールバックされることを確認() {
// Given
Long orderId = 1L;
// When & Then
assertThrows(OrderProcessingException.class, () -> {
orderService.updateOrderStatus(orderId, OrderStatus.FAILED);
});
// トランザクションがロールバックされ、ステータスが変更されていないことを確認
Order order = orderRepository.findById(orderId).orElseThrow();
assertNotEquals(OrderStatus.FAILED, order.getStatus());
}
}
|
まとめと実践Tips#
@Transactionalが効かないパターンの整理#
| パターン |
原因 |
解決策 |
| 自己呼び出し |
プロキシを経由しない |
コンポーネント分離、自己参照取得 |
| privateメソッド |
オーバーライドできない |
publicに変更、コンポーネント分離 |
| 非Spring管理Bean |
プロキシが生成されない |
DIを使用する |
| finalクラス/メソッド |
CGLIBが継承できない |
finalを除去 |
| チェック例外 |
デフォルトでロールバックされない |
rollbackFor属性を指定 |
実践Tips#
-
設計段階でトランザクション境界を明確にする
- サービス層のpublicメソッドをトランザクション境界とする
- 内部処理は別コンポーネントに分離する
-
チェック例外を使用する場合は必ずrollbackForを検討する
- ビジネス例外でもロールバックが必要かどうかを明確にする
-
テストでトランザクション動作を検証する
-
ログを活用してトランザクション動作を可視化する
-
コードレビューで自己呼び出しをチェックする
this.での@Transactionalメソッド呼び出しに注意する
@Transactionalは強力な機能ですが、プロキシベースの実装を理解していないと思わぬ落とし穴にはまります。本記事で解説したパターンと解決策を押さえておくことで、トランザクション管理に関するトラブルを未然に防ぎ、信頼性の高いアプリケーションを構築できます。
参考リンク#