【Java】DBのトランザクション管理方法や落とし穴について

Java

はじめに

Javaで業務システムを開発していると、避けて通れないのが トランザクション管理 です。「@Transactional を付けているから大丈夫」と考えていても、「ロールバックされない」・「データが中途半端に残る」といった事故が発生することがあります。
この記事では、Java(特にSpring)におけるトランザクション管理の基本と落とし穴を、ログとして記載していきたいと思います。

他にも、体系的にJavaを学びたい方には以下の教材がおすすめです:

👉スッキリわかるJava入門 
👉スッキリわかるJava入門 実践編

 

トランザクション管理とは何か

トランザクションとは、一連の処理を「まとめて成功」または「まとめて失敗」にする仕組みです。

【ACID 特性】

特性詳細
Atomicity(原子性)トランザクション内のすべての操作が「全て成功する」か「全て失敗する」かを保証。途中でエラーが発生した場合、トランザクション全体がロールバックされます。
Consistency(一貫性) トランザクションの実行前後でデータベースが常に正しい状態を保つことを保証。整合性制約(例: 外部キーやユニークキー)が常に満たされる状態を維持します。
Isolation(独立性)複数のトランザクションが同時に実行されても、互いに干渉せず独立して処理されることを保証。各トランザクションが単独で実行された場合と同じ結果が得られます。
Durability(永続性)トランザクションが成功した後、その結果が永続的に保存され、システム障害が発生しても失われないことを保証。

 

Springにおけるトランザクション管理方法

現在の実務では、ほとんどが @Transactional を使った宣言的トランザクション管理です。
@Transactionalを使用することによって、明示的に記載しなくても以下の管理が行えます。

  • メソッド開始時にトランザクション開始
  • 例外発生でロールバック
  • 例外発生でロールバック
@Transactional
public void process() {
    orderRepository.save(order);
}

ただし、「例外が発生すれば必ずロールバックされる」わけではありません。ここが最大の落とし穴です。

 

ロールバックされないケース

checked例外はロールバックされない

    @Transactional
    public void insertOrder() throws Exception {
    // 注文テーブルにインサート
        orderRepository.insertOrder("1");

        // 例外で失敗したつもりでもロールバックされない
        throw new Exception("checked exception");
    }

上記処理は、@Transactionalがchecked例外についてはロールバック対象外としているため、以下のようなことが発生してしまいます。

  • insertOrder はDBに反映される(コミットされる可能性がある)
  • 期待した「ロールバック」は発生しないことがある

 

checked例外ではロールバックされないのか

checked例外ではロールバックされない理由は、Springの設計思想としてchecked例外は「業務例外」として扱われる想定だからです。
checked例外は、「コンパイル時にチェックされる例外」「開発者が必ず事前に冷害に対して処理を実施しなければいけない」という思想のため、ロールバックは不要として設計されています。
ただし、実務では業務例外もロールバックしたい場合も発生するため、@Transactionalに任せきりなるのは危険です。

 

対策:rollbackFor を指定する

rollbackForを指定することで、checked例外もロールバックすることが可能です。

    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() throws Exception {
    // 注文テーブルにインサート
        orderRepository.insertOrder("1");

        // 例外で失敗したつもりでもロールバックされない
        throw new Exception("checked exception");
    }

 

try-catchで例外を握りつぶす

    try {
    // 注文テーブルにインサート
        orderRepository.insertOrder("1");

        // ここで何か失敗
        throw new RuntimeException("何かエラーが発生");
    } catch (Exception e) {
        // 例外が外に出ないため、Springは正常と判断してしまう
        log.error("failed", e);
    }

上記処理は、例外を握りつぶしてしまっているため正常終了と判断されて、エラーが発生していてもトランザクションがコミットされたままの状態となってしまう可能性があります。

self-invocation(同一クラス内呼び出し)

@Service
public class OrderService {

    public void importAll() {
        // 同一クラス内の呼び出し
        insertOrder(); // ← @Transactionalが効かない場合がある
    }

    @Transactional
    public void insertOrder() throws Exception {
    // 注文テーブルにインサート
        orderRepository.insertOrder("1");

        // ここで何か失敗
        throw new RuntimeException("何かエラーが発生");
    }
}

self-invocation(同一クラス内呼び出し)で @Transactional が効かないことがあります。例外が出てもトランザクション自体が開始されていないため、ロールバックの概念自体が働きません。

 

トランザクション設計で意識すべきこと

1、例外設計を明確にする

プロジェクト内で例外設計を明確に決めておくのが重要です。

  • 業務例外(checked例外)は例外処理を実装し、ロールバックを実施するのかどうか
  • システム例外(unchecked例外)については、@Transactionalに任せるのか、個別で例外処理を書くのかどうか
  • checked例外は rollbackFor を使用するのか

 

2、トランザクション境界を意識する

クラスは責務管理を設計し、「Controller」や「Repository」に@Transactionalを付けないなど、トランザクションを実施するクラスを明確に分けておくことが肝心です。

 

まとめ

@TransactionaI を付けても、ロールバックされないケースは普通に起きます。

  • checked例外はデフォルトでロールバックされない
  • 例外をcatchして握りつぶすとコミットされる
  • self-invocationでトランザクションが開始されない

「プロジェクト内のロールバック条件」と「例外の扱い」をチームで揃えるという手順を踏んで、トランザクション事故を減らしていきましょう。

 

最後に

Javaの環境構築は、この記事を参照してみてください。
【開発環境構築】VS CodeでJavaを使用するための環境構築を実施する – SEもりのLog (selifemorizo.com)

以上、ログになります。
これからも継続していきましょう!!

Javaサーバーサイド関連
シェアする
おすすめIT本
良いコード/悪いコードで学ぶ設計入門

「ITエンジニア本大賞2023」技術書部門で大賞を受賞した本です。
・コードの可読性
・普段意識したほうが良いこと
・リファクタリング考え方
等、普段のコードを設計する際に意識することが書かれています。
コードのあるべき姿に迷ったら一度読んでみると良い本です。

仕組みと使い方がわかる Docker&Kubernetesのきほんのきほん

Dockerって何?となったときに私が最初に読んだ本です。
Dockerがどんな仕組みで動いているのか、コマンドでは何を命令しているのかを理解できるように、イラストを多用して説明しています。

1冊ですべて身につくJavaScript入門講座

「ITエンジニア本大賞2024」技術書部門で大賞を受賞した本です。
私が次に読もうと思っている本なのでおすすめとして挙げておきたいと思います。

コメント

タイトルとURLをコピーしました