404 Not Found

404 Not Found


nginx

サービス層とトランザクション管理

トランザクションはデータ一貫性の守護者です。注文作成と在庫引当は原子的に完了しなければならず, トランザクション全体が成功するか, 全体がロールバックされます。

1. 学ぶ内容


2. ビジネス開発者の実話

(1) ペインポイント:注文時の在庫引当の不整合

AliceはOrderFlowで深刻なバグを発見しました。ユーザーが注文に成功した後, 商品の在庫が引かれていませんでした。これは注文の作成と在庫引当が2つの別々の操作であり, 注文の作成には成功したが在庫引当に失敗した場合, データの不整合が発生するからです。Charlieはすでに5,000件のオーバーセール注文があり, 会社に10,000ドル以上の損失をもたらしたと報告しました。

(2) 宣言的トランザクションによる解決策

Springの@Transactionalアノテーションにより, トランザクション管理が1行で済みます。

JAVA
@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) 階層アーキテクチャ

100%
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インターフェースと実装

JAVA
// 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"));
    }
}

出力:

TEXT
// 実行成功

4. @Transactional伝播動作

(1) 7つの伝播動作

伝播動作 意味 代表的なシナリオ
REQUIRED (デフォルト) 既存トランザクションに参加, なければ新規作成 大部分のビジネスメソッド
REQUIRES_NEW 常に新規トランザクションを作成し現在のトランザクションを一時停止 監査ログ (外側のロールバックの影響を受けない)
NESTED ネストされたトランザクション (セーブポイント)。外側がロールバックされると内側もロールバック 部分操作を独立してロールバック可能
SUPPORTS トランザクションがあれば参加, なければ非トランザクションで実行 照会メソッド
NOT_SUPPORTED 非トランザクションで実行, 現在のトランザクションを一時停止 トランザクション不要な操作
MANDATORY トランザクション内で呼び出される必要がある, そうでなければ例外 トランザクションが必要なメソッド
NEVER 非トランザクションで呼び出される必要がある, そうでなければ例外 トランザクション操作が許可されない場合
100%
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で監査ログを保証

JAVA
@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);
    }
}

出力:

TEXT
// 実行成功
📌 重要ポイント: REQUIRES_NEWは別のトランザクションで監査ログをコミットするため, 外側のトランザクションがロールバックされてもログは失われません。


5. トランザクションロールバックルール

(1) デフォルトのロールバック動作

デフォルトでは, SpringトランザクションはRuntimeExceptionErrorのみをロールバックし, 検査例外ではロールバックしません

(1) ▶ サンプル:rollbackForの設定

JAVA
@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);
    }
}

出力:

TEXT
// 実行成功
設定 意味 適用シナリオ
rollbackFor = Exception.class すべての例外でロールバック 推奨されるデフォルト設定
rollbackFor = BusinessException.class カスタム例外のみロールバック きめ細かい制御
noRollbackFor = ValidationException.class 指定した例外ではロールバックしない 一部の例外でロールバックを回避

6. 読み取り専用トランザクションの最適化

(1) ▶ サンプル:読み取り専用トランザクション

JAVA
@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);
    }
}

出力:

TEXT
// 実行成功
項目 読み書きトランザクション 読み取り専用トランザクション
ダーティチェック 有効 (フィールドごとの比較) スキップ
パフォーマンス やや遅い 20-30%高速
書き込み操作 許可 禁止 (書き込みで例外発生)
適用 CUD操作 照会操作
💡 ヒント: すべての照会メソッドに@Transactional(readOnly = true)を付けるべきです。これはコストゼロのパフォーマンス最適化です。


7. 注文作成と在庫引当のアトミックトランザクション

(1) ▶ サンプル:完全な注文フロー

JAVA
@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;
    }
}

出力:

TEXT
// 実行成功

8. 総合サンプル:OrderFlowサービス層の完全な実装

JAVA
// 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"));
    }
}

❓ よくある質問

Q @Transactionalアノテーションはインターフェースと実装クラスのどちらに付けるべきですか?
A 実装クラスのメソッドに付けることを推奨します。インターフェースに付けると, CGLIBベースのプロキシはインターフェース上のアノテーションをインターセプトできません。Springも具象クラスに付けることを公式に推奨しています。
Q 同じクラス内のメソッド同士の呼び出しで@Transactionalは有効になりますか?
A なりません。Springトランザクションはプロキシベースであり, 同じクラス内のメソッド呼び出しではプロキシが介在しないため, トランザクションは有効になりません。解決策:1)別のサービスに分割する;2)自分のプロキシをインジェクションする@Lazy private OrderService self;
Q readOnly=trueのときに書き込み操作を行うとどうなりますか?
A データベースとコネクションプールの設定に依存します。Hibernateは例外をスローし, 一部のデータベースドライバは無視します。読み取り専用トランザクション内で書き込み操作を行わないでください。
Q NESTEDとREQUIRES_NEWの違いは何ですか?
A NESTEDはネストされたトランザクション (セーブポイント)で, 外側のトランザクションがロールバックされると内側もロールバックされます。REQUIRES_NEWは独立したトランザクションで, 外側がロールバックされても内側は影響を受けません。独立したコミットにはREQUIRES_NEW, 部分ロールバックにはNESTEDを使用します。
Q トランザクションが失敗するよくある原因は何ですか?
A 1)同じクラス内のメソッド同士の呼び出し;2)メソッドがpublicでない;3)例外がcatchブロックで飲み込まれている;4)デフォルトではRuntimeExceptionのみロールバックし, 検査例外はロールバックしない。
Q トランザクションが有効になっているか確認するにはどうすればよいですか?
A 1)spring.jpa.show-sql=trueを有効にしてSQL実行を監視;2)logging.level.org.springframework.transaction=DEBUGを設定してトランザクションログを確認;3)意図的に例外をスローしてロールバックを確認。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):OrderFlowのProductServiceとOrderServiceに@Transactionalアノテーションを追加し, 注文時の在庫引当の原子性を検証してください。在庫引当後に意図的に例外をスローし, 注文が作成されないことを確認してください。

  2. 応用問題 (難易度 ⭐⭐):AuditServiceを作成し, REQUIRES_NEW伝播動作を使用して, 注文作成トランザクションがロールバックされても監査ログが失われないようにしてください。

  3. チャレンジ問題 (難易度 ⭐⭐⭐):同時注文のシナリオをシミュレートし (在庫1個の商品に対して2つのリクエストが同時に注文する), オーバーセール問題を観察し, 楽観的ロック (@Version)または悲観的ロック (SELECT FOR UPDATE)での解決方法を考察してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%