サービス層とトランザクション管理
トランザクションはデータ一貫性の守護者です。注文作成と在庫引当は原子的に完了しなければならず, トランザクション全体が成功するか, 全体がロールバックされます。
1. 学ぶ内容
- サービス層の設計パターン:インターフェース + 実装,
@Serviceアノテーションと依存性注入 @Transactional伝播動作 (REQUIRED, REQUIRES_NEW, NESTED)- トランザクションロールバックルール:
rollbackFor/noRollbackForの設定 - 読み取り専用トランザクション
@Transactional(readOnly = true)によるパフォーマンス最適化 - Aliceが注文時の在庫引当のアトミックトランザクションを実装
2. ビジネス開発者の実話
(1) ペインポイント:注文時の在庫引当の不整合
AliceはOrderFlowで深刻なバグを発見しました。ユーザーが注文に成功した後, 商品の在庫が引かれていませんでした。これは注文の作成と在庫引当が2つの別々の操作であり, 注文の作成には成功したが在庫引当に失敗した場合, データの不整合が発生するからです。Charlieはすでに5,000件のオーバーセール注文があり, 会社に10,000ドル以上の損失をもたらしたと報告しました。
(2) 宣言的トランザクションによる解決策
Springの@Transactionalアノテーションにより, トランザクション管理が1行で済みます。
@Transactional
public Order createOrder(CreateOrderRequest request) {
Product product = productRepository.findById(request.productId()).orElseThrow();
product.deductStock(request.quantity()); // 在庫の引当
Order order = new Order(product, request.quantity());
return orderRepository.save(order); // 注文の作成
}
在庫引当に失敗すると, トランザクション全体が自動的にロールバックされ, 注文は作成されません。
(3) 成果
Aliceが@Transactionalを追加した後, 注文時の在庫引当が原子性を確保し, オーバーセールの問題が完全に解消されました。Charlieの苦情数はゼロになりました。
3. サービス層の設計パターン
(1) 階層アーキテクチャ
graph TD
A["Controller<br/>リクエスト/レスポンス"] --> B["Service<br/>ビジネスロジック<br/>@Transactional"]
B --> C["Repository<br/>データアクセス"]
C --> D["データベース"]
| 層 | 責務 | 備考 | 命名規則 |
|---|---|---|---|
| Controller | リクエストの受信, パラメータ検証, レスポンス返却 | @RestController |
*Controller |
| Service | ビジネスロジック, トランザクションの調整 | @Service |
*Service / *ServiceImpl |
| Repository | データアクセス | @Repository |
*Repository |
(1) ▶ サンプル:Serviceインターフェースと実装
// OrderService.java (インターフェース)
package com.orderflow.service;
import com.orderflow.model.Order;
public interface OrderService {
Order createOrder(Long productId, Integer quantity);
Order cancelOrder(Long orderId);
Order getOrder(Long orderId);
}
// OrderServiceImpl.java (実装)
package com.orderflow.service;
import com.orderflow.model.*;
import com.orderflow.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public OrderServiceImpl(OrderRepository orderRepository,
ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
@Override
@Transactional
public Order createOrder(Long productId, Integer quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.deductStock(quantity);
Order order = new Order();
order.addItem(product, quantity);
return orderRepository.save(order);
}
@Override
@Transactional(readOnly = true)
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
}
}
出力:
// 実行成功
4. @Transactional伝播動作
(1) 7つの伝播動作
| 伝播動作 | 意味 | 代表的なシナリオ |
|---|---|---|
REQUIRED (デフォルト) |
既存トランザクションに参加, なければ新規作成 | 大部分のビジネスメソッド |
REQUIRES_NEW |
常に新規トランザクションを作成し現在のトランザクションを一時停止 | 監査ログ (外側のロールバックの影響を受けない) |
NESTED |
ネストされたトランザクション (セーブポイント)。外側がロールバックされると内側もロールバック | 部分操作を独立してロールバック可能 |
SUPPORTS |
トランザクションがあれば参加, なければ非トランザクションで実行 | 照会メソッド |
NOT_SUPPORTED |
非トランザクションで実行, 現在のトランザクションを一時停止 | トランザクション不要な操作 |
MANDATORY |
トランザクション内で呼び出される必要がある, そうでなければ例外 | トランザクションが必要なメソッド |
NEVER |
非トランザクションで呼び出される必要がある, そうでなければ例外 | トランザクション操作が許可されない場合 |
sequenceDiagram
participant C as Controller
participant S1 as createOrder (REQUIRED)
participant S2 as recordAudit (REQUIRES_NEW)
participant S3 as deductStock (REQUIRED)
C->>S1: Tx1開始
S1->>S3: Tx1に参加
S3-->>S1: OK
S1->>S2: Tx1一時停止, Tx2開始
S2-->>S1: Tx2コミット, Tx1再開
alt Tx1ロールバック
S1-->>C: Tx1ロールバック (Tx2は影響なし)
end
(1) ▶ サンプル:REQUIRES_NEWで監査ログを保証
@Service
public class AuditService {
private final AuditLogRepository auditLogRepository;
public AuditService(AuditLogRepository auditLogRepository) {
this.auditLogRepository = auditLogRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(String action, String detail) {
AuditLog log = new AuditLog(action, detail, Instant.now());
auditLogRepository.save(log);
}
}
出力:
// 実行成功
REQUIRES_NEWは別のトランザクションで監査ログをコミットするため, 外側のトランザクションがロールバックされてもログは失われません。
5. トランザクションロールバックルール
(1) デフォルトのロールバック動作
デフォルトでは, SpringトランザクションはRuntimeExceptionとErrorのみをロールバックし, 検査例外ではロールバックしません。
(1) ▶ サンプル:rollbackForの設定
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long productId, Integer quantity) throws Exception {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.deductStock(quantity);
Order order = new Order();
order.addItem(product, quantity);
return orderRepository.save(order);
}
}
出力:
// 実行成功
| 設定 | 意味 | 適用シナリオ |
|---|---|---|
rollbackFor = Exception.class |
すべての例外でロールバック | 推奨されるデフォルト設定 |
rollbackFor = BusinessException.class |
カスタム例外のみロールバック | きめ細かい制御 |
noRollbackFor = ValidationException.class |
指定した例外ではロールバックしない | 一部の例外でロールバックを回避 |
6. 読み取り専用トランザクションの最適化
(1) ▶ サンプル:読み取り専用トランザクション
@Service
public class OrderQueryService {
private final OrderRepository orderRepository;
public OrderQueryService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional(readOnly = true)
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
@Transactional(readOnly = true)
public Page<Order> listOrders(String status, Pageable pageable) {
return orderRepository.findByStatus(status, pageable);
}
}
出力:
// 実行成功
| 項目 | 読み書きトランザクション | 読み取り専用トランザクション |
|---|---|---|
| ダーティチェック | 有効 (フィールドごとの比較) | スキップ |
| パフォーマンス | やや遅い | 20-30%高速 |
| 書き込み操作 | 許可 | 禁止 (書き込みで例外発生) |
| 適用 | CUD操作 | 照会操作 |
@Transactional(readOnly = true)を付けるべきです。これはコストゼロのパフォーマンス最適化です。
7. 注文作成と在庫引当のアトミックトランザクション
(1) ▶ サンプル:完全な注文フロー
@Service
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final AuditService auditService;
public OrderServiceImpl(OrderRepository orderRepository,
ProductRepository productRepository,
AuditService auditService) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.auditService = auditService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long productId, Integer quantity) {
// 1. 商品を検索
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found: " + productId));
// 2. 在庫を確認
if (product.getStock() < quantity) {
throw new RuntimeException("Insufficient stock: available="
+ product.getStock() + ", requested=" + quantity);
}
// 3. 在庫を引当 (同じトランザクション内)
product.deductStock(quantity);
// 4. 注文と注文明細を作成
Order order = new Order();
order.addItem(product, quantity);
Order saved = orderRepository.save(order);
// 5. 監査ログ (REQUIRES_NEWで別トランザクション)
auditService.logAudit("CREATE_ORDER",
"Order " + saved.getId() + " created for product " + productId);
return saved;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Order cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found: " + orderId));
if (!"PENDING".equals(order.getStatus())) {
throw new RuntimeException("Cannot cancel order with status: " + order.getStatus());
}
// 各項目の在庫を復元
for (OrderItem item : order.getItems()) {
item.getProduct().addStock(item.getQuantity());
}
order.setStatus("CANCELLED");
auditService.logAudit("CANCEL_ORDER", "Order " + orderId + " cancelled");
return order;
}
}
出力:
// 実行成功
8. 総合サンプル:OrderFlowサービス層の完全な実装
// ProductService.java
package com.orderflow.service;
import com.orderflow.model.Product;
import com.orderflow.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional(readOnly = true)
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
@Transactional(readOnly = true)
public Page<Product> listProducts(Pageable pageable) {
return productRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public List<Product> searchProducts(String keyword) {
return productRepository.findByNameContaining(keyword);
}
@Transactional
public Product createProduct(String name, BigDecimal price, Integer stock) {
return productRepository.save(new Product(name, price, stock));
}
@Transactional
public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
Product product = getProduct(id);
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return product;
}
}
// OrderService.java
package com.orderflow.service;
import com.orderflow.model.Order;
import com.orderflow.model.Product;
import com.orderflow.repository.OrderRepository;
import com.orderflow.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long productId, Integer quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
if (product.getStock() < quantity) {
throw new RuntimeException("Insufficient stock");
}
product.deductStock(quantity);
Order order = new Order();
order.addItem(product, quantity);
return orderRepository.save(order);
}
@Transactional(rollbackFor = Exception.class)
public Order cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
order.getItems().forEach(item -> item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
return order;
}
@Transactional(readOnly = true)
public Order getOrder(Long orderId) {
return orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
}
}
❓ よくある質問
@Lazy private OrderService self;。readOnly=trueのときに書き込み操作を行うとどうなりますか?spring.jpa.show-sql=trueを有効にしてSQL実行を監視;2)logging.level.org.springframework.transaction=DEBUGを設定してトランザクションログを確認;3)意図的に例外をスローしてロールバックを確認。📖 まとめ
- Service層はビジネスロジックをカプセル化。ControllerはServiceを呼び出し, ServiceはRepositoryを呼び出す
@TransactionalデフォルトはREQUIRED伝播。RuntimeExceptionは自動的にロールバックREQUIRES_NEWは独立したトランザクション (監査ログなど)に使用,NESTEDはネストされたトランザクションに使用rollbackFor = Exception.classですべての例外でロールバックを保証@Transactional(readOnly = true)は照会メソッドのコストゼロの最適化- 同じクラス内のメソッド同士の呼び出しではトランザクションは有効にならない。Springプロキシメカニズムの制限
📝 練習問題
-
基本問題 (難易度 ⭐):OrderFlowのProductServiceとOrderServiceに
@Transactionalアノテーションを追加し, 注文時の在庫引当の原子性を検証してください。在庫引当後に意図的に例外をスローし, 注文が作成されないことを確認してください。 -
応用問題 (難易度 ⭐⭐):AuditServiceを作成し,
REQUIRES_NEW伝播動作を使用して, 注文作成トランザクションがロールバックされても監査ログが失われないようにしてください。 -
チャレンジ問題 (難易度 ⭐⭐⭐):同時注文のシナリオをシミュレートし (在庫1個の商品に対して2つのリクエストが同時に注文する), オーバーセール問題を観察し, 楽観的ロック (
@Version)または悲観的ロック (SELECT FOR UPDATE)での解決方法を考察してください。



