Spring Bootのトランザクション入門|@Transactionalとロールバックを解説

Spring BootでDB更新処理を書くようになると、@Transactional を見かける機会が増えてきます。

ただ、最初のうちは「とりあえず付けるもの」として覚えやすく、なぜ必要なのか、
どんなときにロールバックされるのか、どこに付けるべきなのかが曖昧なまま進みがちです。

Spring Frameworkでは、宣言的トランザクション管理の中心として @Transactional を使えます。

考え方としては、メソッド単位でトランザクション境界を定義し、通常は PROPAGATION_REQUIRED で動作します。

この記事では、Spring Bootでトランザクションをどう考えるべきかを、入門者向けに整理します。
@Transactional の基本から、ロールバックのルール、ハマりやすいポイントまで説明します。

目次

トランザクションが必要な理由

複数の更新処理は途中失敗が危ない

たとえば、購入処理で次の2つを行うとします。

  1. 注文データを保存する
  2. 在庫を減らす

このとき、注文だけ保存されて在庫更新が失敗すると、データの整合性が崩れます。
逆に、在庫だけ減って注文が残らない状態も困ります。

こういう処理では、全部成功するか、全部取り消しされるかのAll or nothingが原則です。

トランザクションで防げること

トランザクションを使うと、処理の途中で失敗したときに、それまでの更新をまとめて戻せます。
Spring Frameworkでも、失敗時に宣言的にロールバックを扱える仕組みが提供されています。

トランザクションは便利機能というより、更新処理で不整合を起こさないための土台として理解しましょう。

@Transactional の基本

ここでは、@Transactional をの使い方を整理します。

まずはServiceに付ける

入門段階では、@TransactionalServiceの更新メソッドに付けると考えるのがいちばんわかりやすいです。

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final StockRepository stockRepository;

    public OrderService(OrderRepository orderRepository,
                        StockRepository stockRepository) {
        this.orderRepository = orderRepository;
        this.stockRepository = stockRepository;
    }

    @Transactional
    public void purchase(Long productId, int quantity) {
        // 注文を保存
        orderRepository.save(new Order(productId, quantity));

        // 在庫を減らす
        stockRepository.decrease(productId, quantity);
    }
}

複数の更新処理をまとめたいなら、そのまとまりを表すServiceメソッドに付けるのが自然です。
Controllerに付けるより、業務処理の責務が見えやすくなります。

メソッド単位で処理をまとめる

@Transactional は、SQLごとに区切るというより、メソッド全体を一つの処理単位として扱うイメージです。

Spring Frameworkの宣言的トランザクション管理も、メソッド単位で挙動を定義する前提で整理されています。

Repositoryごとに細かく付けるより、業務処理をまとめたServiceメソッドに付けたほうが、
「何を一つの成功 / 失敗として扱いたいのか」が見えやすくなります。

付ければ何でも安全になるわけではない

ここは最初に知っておいたほうがいいところです。
@Transactional は便利ですが、付けるだけで全部うまくいくわけではありません。

例外の種類でロールバック条件は変わりますし、呼び出し方によっては想定通りに適用されないこともあります。

Spring Frameworkでも、ロールバックルールやプロキシ経由での適用が前提になっています。

ロールバックの基本ルール

RuntimeException はロールバックされる

Spring Frameworkのデフォルトでは、RuntimeExceptionError はロールバック対象です。

@Service
public class OrderService {

    @Transactional
    public void purchase() {
        // DB更新処理

        throw new IllegalStateException("購入処理に失敗しました");
    }
}

この例の IllegalStateExceptionRuntimeException の一種なので、通常はロールバックされます。

ここはかなり大事です。
「例外が出たら全部戻る」と覚えると、あとでズレやすくなります。
まずは、unchecked exception が基本でロールバックされると押さえるのがわかりやすいです。

checked exception はそのままだとロールバックされない

一方で、checked exception はデフォルトではロールバック対象ではありません。

@Service
public class OrderService {

    @Transactional
    public void purchase() throws Exception {
        // DB更新処理

        throw new Exception("チェック例外です");
    }
}

このコードでは Exception を投げていますが、これは checked exception なので、
そのままではロールバックされません。

このあたりは初学者がかなりハマりやすいところです。

失敗したのにDB更新が残っている場合は、まず投げた例外が checked exception ではないか確認しましょう。

rollbackFor で明示できる

checked exception でもロールバックしたい場合は、rollbackFor を使って明示できます。
Spring Frameworkでも、ロールバックルールを設定することで checked exception をロールバック対象にできます。

@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void purchase() throws Exception {
        // DB更新処理

        throw new Exception("チェック例外でもロールバックしたい");
    }
}

このようにしておけば、checked exception でもロールバック対象にできます。

ただ、何でも Exception.class にしておけばよいわけではありません。
大事なのは、どの失敗を処理全体の失敗として戻したいのかを先に整理することです。

実務でハマりやすい注意点

ここでは、@Transactional を使うときに見落としやすい点を整理します。

同じクラス内呼び出しでは効かないことがある

Springの @Transactional は、デフォルトではプロキシ経由の呼び出しで適用されます。

そのため、同じクラス内で this.xxx() のように呼び出す self-invocation では、
期待したトランザクション適用にならないことがあります。

@Service
public class OrderService {

    public void execute() {
        // 同じクラス内呼び出し
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // self-invocation では
        // 想定どおり効かないことがある
    }
}

このケースは、@Transactional を付けたのに効いていないように見える典型例です。
対策としては、トランザクションをかけたい処理を別Serviceに分けるほうが、設計としてもわかりやすくなります。

例外を握りつぶすと意図どおりに戻らない

もう1つよくあるのが、例外を catch して握りつぶしてしまうケースです。

@Service
public class OrderService {

    @Transactional
    public void purchase() {
        try {
            // DB更新処理
            throw new IllegalStateException("失敗");
        } catch (Exception e) {
            // ログだけ出して終了してしまう
            System.out.println(e.getMessage());
        }
    }
}

このようなコードだと、呼び出し元からは正常終了したように見えます。
その結果、ロールバックを期待していたのに思った通りに戻らないことがあります。

ロールバック判定は、最終的にどう例外が外へ出るかと強く関わります。Spring Frameworkでも、
ロールバックルールは例外型をもとに判定されます。

伝播属性はまず REQUIRED を理解する

@Transactional には propagation という設定があります。
これは、すでにトランザクションがある状態で別のトランザクション対象メソッドを呼んだときに、
どう参加するかを決めるものです。

入門段階では、まず**デフォルトが REQUIRED**だと押さえておけば十分です。
Spring Frameworkでも、デフォルトの伝播属性は REQUIRED です。

ここを最初から広げすぎると重くなりやすいので、
まずは「通常は既存のトランザクションに参加する」と理解しておくと整理しやすいです。

まとめ

Spring Bootのトランザクションは、複数の更新処理を安全にまとめるための仕組みです。

@Transactional は便利ですが、ただ付けるだけではなく、どのメソッドに付けるか、
どの例外でロールバックされるか、適用される呼び出し方になっているかまで理解しておくことが大切です。

最初に押さえたいポイントは次のとおりです。

  1. @Transactional はまずServiceの更新メソッドに付ける
  2. RuntimeException はロールバック、checked exception はデフォルトではロールバックしない
  3. 同じクラス内呼び出しや例外の握りつぶしで、期待どおりに動かないことがある

この3つが整理できるだけでも、Spring Bootの更新処理はかなり安定して書きやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次