Spring Frameworkにおけるトランザクション管理の中核を担う@Transactionalアノテーションは、宣言的トランザクション制御を実現する強力な機能です。本記事では、Spring @Transactionalの基本的な使い方から、トランザクション伝播(Propagation)の各属性の動作と使い分け、分離レベル(Isolation)の設定によるデータ整合性の確保、そしてreadOnlyフラグによるパフォーマンス最適化まで、実務で必要となるトランザクション設計の知識を体系的に解説します。適切なトランザクション境界の設計は、データの整合性とアプリケーションのパフォーマンスを両立させる上で不可欠な要素です。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| 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依存関係の追加
@Transactionalの基本と使い方#
@Transactionalアノテーションは、メソッドまたはクラスに付与することで、そのスコープ内の処理をトランザクション境界として定義します。Spring Bootでは@EnableTransactionManagementがAuto Configurationにより自動で有効化されるため、追加設定なしで利用できます。
基本的な使い方#
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
|
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
public OrderService(OrderRepository orderRepository,
PaymentRepository paymentRepository) {
this.orderRepository = orderRepository;
this.paymentRepository = paymentRepository;
}
@Transactional
public Order createOrder(OrderRequest request) {
// 注文の作成
Order order = new Order(request.getCustomerId(), request.getItems());
orderRepository.save(order);
// 支払い情報の作成
Payment payment = new Payment(order.getId(), request.getPaymentMethod());
paymentRepository.save(payment);
// 例外が発生した場合、両方の操作がロールバックされる
return order;
}
}
|
期待される結果: createOrderメソッド内で例外が発生した場合、OrderとPaymentの両方の保存処理がロールバックされ、データの整合性が保たれます。
クラスレベルとメソッドレベルの適用#
@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
|
@Service
@Transactional(readOnly = true) // クラスレベル:参照系はreadOnly
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// クラスレベルの設定(readOnly = true)が適用される
public Product findById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
public List<Product> findAll() {
return productRepository.findAll();
}
// メソッドレベルの設定が優先される(readOnly = false)
@Transactional
public Product updateProduct(Long id, ProductUpdateRequest request) {
Product product = findById(id);
product.updateName(request.getName());
product.updatePrice(request.getPrice());
return product; // ダーティチェックにより自動更新
}
}
|
@Transactionalのデフォルト設定#
@Transactionalのデフォルト設定は以下の通りです。
| 属性 |
デフォルト値 |
説明 |
| propagation |
REQUIRED |
既存トランザクションがあれば参加、なければ新規作成 |
| isolation |
DEFAULT |
データベースのデフォルト分離レベルを使用 |
| readOnly |
false |
読み書き可能なトランザクション |
| timeout |
-1(無制限) |
タイムアウトなし |
| rollbackFor |
RuntimeException、Error |
これらの例外でロールバック |
| noRollbackFor |
なし |
指定した例外ではロールバックしない |
伝播属性(Propagation)の種類と動作例#
トランザクション伝播(Propagation)は、既存のトランザクションが存在する場合の振る舞いを制御します。適切な伝播属性の選択は、複雑なビジネスロジックにおけるトランザクション境界の設計に不可欠です。
Propagation一覧#
| 伝播属性 |
説明 |
ユースケース |
REQUIRED |
既存トランザクションに参加、なければ新規作成 |
一般的なビジネスロジック |
REQUIRES_NEW |
常に新規トランザクションを作成(既存は一時停止) |
監査ログ、独立した処理 |
NESTED |
既存トランザクション内にセーブポイントを作成 |
部分的なロールバックが必要な処理 |
SUPPORTS |
トランザクションがあれば参加、なければ非トランザクション |
参照系で柔軟性が必要な場合 |
NOT_SUPPORTED |
非トランザクションで実行(既存は一時停止) |
トランザクション外で実行したい処理 |
MANDATORY |
既存トランザクション必須、なければ例外 |
必ずトランザクション内で呼ばれるべき処理 |
NEVER |
トランザクション禁止、あれば例外 |
トランザクション外での実行を強制 |
PROPAGATION_REQUIRED(デフォルト)#
最も一般的に使用される伝播属性です。既存のトランザクションがあれば参加し、なければ新規に作成します。
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
|
@Service
public class OrderProcessingService {
private final OrderService orderService;
private final InventoryService inventoryService;
@Transactional // REQUIRED(デフォルト)
public void processOrder(OrderRequest request) {
// 1. 注文作成(同一トランザクションに参加)
Order order = orderService.createOrder(request);
// 2. 在庫減少(同一トランザクションに参加)
inventoryService.decreaseStock(request.getItems());
// いずれかで例外が発生すると、全体がロールバック
}
}
@Service
public class OrderService {
@Transactional // 既存トランザクションに参加
public Order createOrder(OrderRequest request) {
// ...
}
}
@Service
public class InventoryService {
@Transactional // 既存トランザクションに参加
public void decreaseStock(List<OrderItem> items) {
// ...
}
}
|
sequenceDiagram
participant Client
participant OrderProcessingService
participant OrderService
participant InventoryService
participant DB
Client->>OrderProcessingService: processOrder()
Note over OrderProcessingService: トランザクション開始
OrderProcessingService->>OrderService: createOrder()
Note over OrderService: 既存トランザクションに参加
OrderService->>DB: INSERT Order
OrderService-->>OrderProcessingService: Order
OrderProcessingService->>InventoryService: decreaseStock()
Note over InventoryService: 既存トランザクションに参加
InventoryService->>DB: UPDATE Inventory
InventoryService-->>OrderProcessingService: void
Note over OrderProcessingService: トランザクションコミット
OrderProcessingService-->>Client: 完了PROPAGATION_REQUIRES_NEW#
常に新しいトランザクションを作成し、既存のトランザクションは一時停止されます。内部トランザクションの結果が外部トランザクションに影響しない独立した処理に使用します。
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
41
|
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final AuditLogService auditLogService;
@Transactional
public void processPayment(PaymentRequest request) {
try {
Payment payment = executePayment(request);
paymentRepository.save(payment);
// 監査ログは独立したトランザクションで記録
// メイン処理が失敗しても監査ログは残る
auditLogService.logPaymentAttempt(request, "SUCCESS");
} catch (PaymentException e) {
// 支払い失敗時も監査ログは記録される
auditLogService.logPaymentAttempt(request, "FAILED");
throw e; // メイントランザクションはロールバック
}
}
}
@Service
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPaymentAttempt(PaymentRequest request, String status) {
AuditLog log = new AuditLog(
"PAYMENT",
request.getUserId(),
status,
LocalDateTime.now()
);
auditLogRepository.save(log);
// この保存は独立してコミットされる
}
}
|
sequenceDiagram
participant Client
participant PaymentService
participant AuditLogService
participant DB
Client->>PaymentService: processPayment()
Note over PaymentService: トランザクション1 開始
PaymentService->>DB: INSERT Payment
PaymentService->>AuditLogService: logPaymentAttempt()
Note over PaymentService: トランザクション1 一時停止
Note over AuditLogService: トランザクション2 開始(新規)
AuditLogService->>DB: INSERT AuditLog
Note over AuditLogService: トランザクション2 コミット
AuditLogService-->>PaymentService: 完了
Note over PaymentService: トランザクション1 再開
alt 成功時
Note over PaymentService: トランザクション1 コミット
else 失敗時
Note over PaymentService: トランザクション1 ロールバック
Note right of DB: AuditLogは残る
end
PaymentService-->>Client: 結果注意点: REQUIRES_NEWを使用すると、外部トランザクションと内部トランザクションで別々のデータベースコネクションが必要になります。コネクションプールのサイズが小さい場合、デッドロックが発生する可能性があります。
PROPAGATION_NESTED#
既存トランザクション内にセーブポイントを作成し、部分的なロールバックを可能にします。JDBCのセーブポイント機能を使用するため、JDBCリソーストランザクションでのみ動作します。
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
|
@Service
public class BatchOrderService {
private final OrderRepository orderRepository;
@Transactional
public BatchResult processBatchOrders(List<OrderRequest> requests) {
BatchResult result = new BatchResult();
for (OrderRequest request : requests) {
try {
// 各注文処理はNESTEDトランザクション
Order order = processingSingleOrder(request);
result.addSuccess(order);
} catch (OrderProcessingException e) {
// この注文のみロールバック、他の注文処理は継続
result.addFailure(request, e.getMessage());
}
}
// 成功した注文のみコミットされる
return result;
}
@Transactional(propagation = Propagation.NESTED)
public Order processingSingleOrder(OrderRequest request) {
// バリデーション
validateOrder(request);
Order order = new Order(request);
orderRepository.save(order);
// 何らかの問題で例外が発生した場合
// このセーブポイント以降のみロールバック
return order;
}
}
|
flowchart TB
subgraph OuterTx["外部トランザクション"]
Start["processBatchOrders開始"]
subgraph Nested1["NESTEDトランザクション1"]
SP1["セーブポイント1"]
Order1["Order1処理"]
Commit1["成功 → 継続"]
end
subgraph Nested2["NESTEDトランザクション2"]
SP2["セーブポイント2"]
Order2["Order2処理"]
Rollback2["失敗 → セーブポイントまでロールバック"]
end
subgraph Nested3["NESTEDトランザクション3"]
SP3["セーブポイント3"]
Order3["Order3処理"]
Commit3["成功 → 継続"]
end
FinalCommit["外部トランザクションコミット"]
end
Start --> SP1 --> Order1 --> Commit1
Commit1 --> SP2 --> Order2 --> Rollback2
Rollback2 --> SP3 --> Order3 --> Commit3
Commit3 --> FinalCommit
Result["結果: Order1とOrder3のみ保存"]
FinalCommit --> ResultPROPAGATION_SUPPORTS / NOT_SUPPORTED / MANDATORY / NEVER#
これらの伝播属性は特定のユースケースで使用します。
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
|
@Service
public class ReportService {
// SUPPORTS: トランザクションがあれば参加、なければ非トランザクション
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Report generateReport(ReportCriteria criteria) {
// トランザクション有無に関わらず動作
return reportRepository.findByCriteria(criteria);
}
}
@Service
public class ExternalApiService {
// NOT_SUPPORTED: 常に非トランザクションで実行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public ApiResponse callExternalApi(ApiRequest request) {
// 外部API呼び出しはトランザクション外で実行
// 長時間のDB接続保持を避ける
return externalApiClient.call(request);
}
}
@Service
public class InternalProcessingService {
// MANDATORY: 既存トランザクション必須
@Transactional(propagation = Propagation.MANDATORY)
public void processInternal(ProcessingData data) {
// 呼び出し元がトランザクションを開始していない場合は例外
// IllegalTransactionStateException がスローされる
}
}
|
分離レベル(Isolation)の種類と選定基準#
分離レベル(Isolation Level)は、同時実行されるトランザクション間でのデータの可視性を制御します。適切な分離レベルの選択は、データ整合性とパフォーマンスのバランスを取る上で重要です。
分離レベル一覧と読み取り現象#
| 分離レベル |
ダーティリード |
非反復読み取り |
ファントムリード |
説明 |
READ_UNCOMMITTED |
発生する |
発生する |
発生する |
最も低い分離レベル |
READ_COMMITTED |
防止 |
発生する |
発生する |
PostgreSQLのデフォルト |
REPEATABLE_READ |
防止 |
防止 |
発生する |
MySQLのデフォルト |
SERIALIZABLE |
防止 |
防止 |
防止 |
最も高い分離レベル |
読み取り現象の説明#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// ダーティリード(Dirty Read)の例
// トランザクションAがコミット前のデータをトランザクションBが読み取る
// トランザクションA
@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = accountRepository.findById(accountId).orElseThrow();
account.setBalance(amount); // まだコミットされていない
// この時点でトランザクションBがこの値を読み取る可能性(ダーティリード)
validateBalance(account); // バリデーションで例外 → ロールバック
}
// トランザクションB(READ_UNCOMMITTEDの場合)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public BigDecimal getBalance(Long accountId) {
// トランザクションAのコミット前の値を読み取る可能性あり
return accountRepository.findById(accountId)
.map(Account::getBalance)
.orElse(BigDecimal.ZERO);
}
|
分離レベルの設定例#
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
41
42
43
|
@Service
public class AccountService {
private final AccountRepository accountRepository;
// READ_COMMITTED: 一般的な参照処理
@Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
public Account findAccount(Long id) {
return accountRepository.findById(id)
.orElseThrow(() -> new AccountNotFoundException(id));
}
// REPEATABLE_READ: 同一トランザクション内で一貫した読み取りが必要
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
// 複数回の読み取りでも同じ値が保証される
BigDecimal fromBalance = from.getBalance();
// 何らかの処理...
// 再度読み取っても同じ値(非反復読み取りが防止される)
BigDecimal fromBalanceAgain = accountRepository.findById(fromId)
.map(Account::getBalance)
.orElse(BigDecimal.ZERO);
// fromBalance == fromBalanceAgain が保証される
from.withdraw(amount);
to.deposit(amount);
}
// SERIALIZABLE: 厳密な整合性が必要な金融処理
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalFinancialOperation(Long accountId, BigDecimal amount) {
// 完全な直列化が保証される
// 他のトランザクションとの競合時はリトライが必要
Account account = accountRepository.findByIdWithLock(accountId);
account.adjustBalance(amount);
}
}
|
分離レベル選定のガイドライン#
flowchart TD
Start["分離レベルの選択"] --> Q1{"ダーティリードを<br/>許容できるか?"}
Q1 -->|Yes| RU["READ_UNCOMMITTED<br/>(ほぼ使用しない)"]
Q1 -->|No| Q2{"同一トランザクション内で<br/>一貫した読み取りが必要か?"}
Q2 -->|No| RC["READ_COMMITTED<br/>(推奨デフォルト)"]
Q2 -->|Yes| Q3{"ファントムリードを<br/>防止する必要があるか?"}
Q3 -->|No| RR["REPEATABLE_READ"]
Q3 -->|Yes| SR["SERIALIZABLE<br/>(パフォーマンス低下に注意)"]
RC --> Note1["一般的なWebアプリケーション<br/>PostgreSQLのデフォルト"]
RR --> Note2["レポート生成<br/>集計処理"]
SR --> Note3["金融取引<br/>在庫管理の厳密な処理"]選定基準のポイント:
- READ_COMMITTED: 多くのWebアプリケーションで十分。PostgreSQLのデフォルト
- REPEATABLE_READ: 同一トランザクション内での一貫性が重要な集計処理
- SERIALIZABLE: 金融システムなど厳密な整合性が必須の場合(パフォーマンスコストを考慮)
readOnlyフラグの効果とパフォーマンス最適化#
readOnly = trueフラグは、トランザクションが読み取り専用であることをフレームワークとデータベースに通知します。これにより複数の最適化が適用されます。
readOnlyフラグの効果#
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
@Transactional(readOnly = true) // クラスレベルで設定
public class ProductQueryService {
private final ProductRepository productRepository;
public ProductQueryService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// readOnly = true の効果:
// 1. Hibernateのダーティチェックが無効化される
// 2. FlushModeがMANUALに設定される(自動フラッシュなし)
// 3. データベースが読み取り専用の最適化を適用できる
public List<Product> findByCategory(String category) {
return productRepository.findByCategory(category);
}
public Page<Product> searchProducts(ProductSearchCriteria criteria,
Pageable pageable) {
return productRepository.findAll(
ProductSpecifications.byCriteria(criteria),
pageable
);
}
// 大量データの読み取りで特に効果的
public List<ProductSummary> findAllSummaries() {
return productRepository.findAllSummaries();
}
}
|
パフォーマンス比較#
| 観点 |
readOnly = false |
readOnly = true |
| ダーティチェック |
有効(オーバーヘッドあり) |
無効 |
| FlushMode |
AUTO(自動フラッシュ) |
MANUAL(フラッシュなし) |
| スナップショット保持 |
保持する(メモリ使用) |
保持しない |
| DB最適化 |
なし |
レプリカへのルーティング等 |
実践的な使用パターン#
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
41
42
|
@Service
public class ReportService {
private final OrderRepository orderRepository;
private final EntityManager entityManager;
public ReportService(OrderRepository orderRepository,
EntityManager entityManager) {
this.orderRepository = orderRepository;
this.entityManager = entityManager;
}
// 大量データを扱うレポート生成
@Transactional(readOnly = true)
public SalesReport generateMonthlySalesReport(YearMonth month) {
LocalDate startDate = month.atDay(1);
LocalDate endDate = month.atEndOfMonth();
// 大量のOrderを読み取り
List<Order> orders = orderRepository
.findByOrderDateBetween(startDate, endDate);
// readOnly = true により:
// - 各Orderのスナップショットが保持されない
// - ダーティチェックのオーバーヘッドがない
// - メモリ使用量が削減される
return SalesReport.from(orders);
}
// ストリーミング処理との組み合わせ
@Transactional(readOnly = true)
public void exportAllOrders(OutputStream outputStream) {
try (Stream<Order> orderStream = orderRepository.streamAll()) {
orderStream.forEach(order -> {
writeToStream(order, outputStream);
// メモリ解放のため定期的にクリア
entityManager.detach(order);
});
}
}
}
|
readOnlyとデータベースレプリカ#
Spring Boot 3.x + HikariCPでは、readOnlyトランザクションを自動的にリードレプリカにルーティングする設定が可能です。
1
2
3
4
5
6
7
8
9
10
11
|
# application.yml
spring:
datasource:
primary:
url: jdbc:postgresql://primary-db:5432/myapp
username: app_user
password: ${DB_PASSWORD}
replica:
url: jdbc:postgresql://replica-db:5432/myapp
username: app_user
password: ${DB_PASSWORD}
|
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
|
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("primaryDataSource") DataSource primary,
@Qualifier("replicaDataSource") DataSource replica) {
ReadWriteRoutingDataSource routingDataSource =
new ReadWriteRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.PRIMARY, primary);
targetDataSources.put(DataSourceType.REPLICA, replica);
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(primary);
return routingDataSource;
}
}
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// readOnlyトランザクションはレプリカへ
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? DataSourceType.REPLICA
: DataSourceType.PRIMARY;
}
}
|
よくある誤解とアンチパターン#
@Transactionalの使用において、多くの開発者が陥りやすい問題点とその対策を解説します。
アンチパターン1: 自己呼び出し(Self-Invocation)#
Springのトランザクション管理はプロキシベースで動作するため、同一クラス内でのメソッド呼び出しではトランザクションが適用されません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 悪い例: 自己呼び出しでトランザクションが効かない
@Service
public class OrderService {
@Transactional
public void processOrders(List<OrderRequest> requests) {
for (OrderRequest request : requests) {
// 自己呼び出し - @Transactionalが無視される
createOrder(request);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(OrderRequest request) {
// REQUIRES_NEWが適用されない
// processOrdersと同一トランザクションで実行される
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 良い例1: 別のBeanに分離
@Service
public class OrderService {
private final OrderCreationService orderCreationService;
@Transactional
public void processOrders(List<OrderRequest> requests) {
for (OrderRequest request : requests) {
// 別Beanの呼び出し - トランザクションが正しく適用される
orderCreationService.createOrder(request);
}
}
}
@Service
public class OrderCreationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(OrderRequest request) {
// REQUIRES_NEWが正しく適用される
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 良い例2: 自己注入(Self-Injection)を使用
@Service
public class OrderService {
private final OrderService self;
public OrderService(@Lazy OrderService self) {
this.self = self;
}
@Transactional
public void processOrders(List<OrderRequest> requests) {
for (OrderRequest request : requests) {
// プロキシ経由の呼び出し
self.createOrder(request);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(OrderRequest request) {
// REQUIRES_NEWが正しく適用される
}
}
|
アンチパターン2: チェック例外でのロールバック漏れ#
デフォルトではRuntimeExceptionとErrorのみがロールバック対象です。チェック例外ではロールバックされません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 悪い例: チェック例外でロールバックされない
@Service
public class PaymentService {
@Transactional
public void processPayment(PaymentRequest request) throws PaymentException {
paymentRepository.save(new Payment(request));
// PaymentExceptionはチェック例外
// この例外が発生してもトランザクションはコミットされる
if (!validatePayment(request)) {
throw new PaymentException("Payment validation failed");
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 良い例: rollbackForを明示的に指定
@Service
public class PaymentService {
@Transactional(rollbackFor = PaymentException.class)
public void processPayment(PaymentRequest request) throws PaymentException {
paymentRepository.save(new Payment(request));
if (!validatePayment(request)) {
throw new PaymentException("Payment validation failed");
// PaymentExceptionでもロールバックされる
}
}
}
// または全ての例外でロールバック
@Service
public class PaymentService {
@Transactional(rollbackFor = Exception.class)
public void processPayment(PaymentRequest request) throws PaymentException {
// ...
}
}
|
アンチパターン3: 長時間トランザクション#
トランザクション内で外部API呼び出しや長時間処理を行うと、データベースコネクションが長時間占有されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 悪い例: トランザクション内で外部API呼び出し
@Service
public class OrderService {
@Transactional
public Order createOrderWithNotification(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
// 外部API呼び出し - レスポンスを待つ間DBコネクション占有
notificationService.sendEmail(order.getCustomerEmail());
// 外部サービス障害時にトランザクションも失敗
inventoryService.callExternalInventoryApi(order.getItems());
return order;
}
}
|
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
|
// 良い例: トランザクション外で外部処理を実行
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
// トランザクション完了後にイベント発行
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
@Component
public class OrderEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCreated(OrderCreatedEvent event) {
// トランザクション外で非同期実行
notificationService.sendEmail(event.getOrder().getCustomerEmail());
}
}
|
アンチパターン4: 不適切な分離レベルの使用#
1
2
3
4
5
6
7
8
9
10
|
// 悪い例: 全てにSERIALIZABLEを設定
@Service
@Transactional(isolation = Isolation.SERIALIZABLE)
public class ProductService {
// 単純な参照にSERIALIZABLEは過剰
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 良い例: 用途に応じた分離レベル
@Service
public class ProductService {
// 参照系はREAD_COMMITTEDで十分
@Transactional(readOnly = true,
isolation = Isolation.READ_COMMITTED)
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
// 厳密な整合性が必要な処理のみSERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateInventoryCount(Long productId, int count) {
// ...
}
}
|
まとめと実践Tips#
本記事では、Spring @Transactionalアノテーションの基本から、トランザクション伝播、分離レベル、readOnly最適化、そしてアンチパターンまでを解説しました。
実践Tips#
-
クラスレベルでreadOnly = trueを設定し、更新系メソッドのみ@Transactionalでオーバーライド
-
伝播属性の選択基準
- 通常:
REQUIRED(デフォルト)
- 監査ログ等の独立処理:
REQUIRES_NEW
- 部分ロールバック:
NESTED
-
分離レベルはデフォルト(READ_COMMITTED)を基本に、必要な箇所のみ上げる
SERIALIZABLEはパフォーマンスコストを十分に検討
-
チェック例外を使用する場合はrollbackForを明示的に指定
-
自己呼び出しを避け、トランザクション境界を意識した設計を行う
-
外部API呼び出しはトランザクション外で実行
@TransactionalEventListenerの活用
-
テストで@Transactionalの動作を検証
適切なトランザクション設計は、システムの信頼性とパフォーマンスを大きく左右します。本記事の内容を参考に、プロジェクトの要件に合ったトランザクション戦略を構築してください。
参考リンク#