LoginSignup
0

More than 3 years have passed since last update.

transaction使うときPDOExceptionをどうハンドリングするべきなのかいまさら考える

Last updated at Posted at 2020-12-20

昔えらいひとが言ってた気がする

PDO使っててtransaction張る時、

function hoge(PDO $connection) {
    try {
        $connection->beginTransaction();
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        $connection->rollBack();
        {例外時後処理}
    }
}

諸所の処理に紛れてたまにこんな構造になったりしてませんか?
これは厳密なケースに対応しきれずよろしくありません。

function hoge(PDO $connection) {
    $connection->beginTransaction(); // try句から掃き出す
    try {
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        $connection->rollBack();
        {例外時後処理}
    }
}

beginTransaction()はtry句に入れないようにします。

…とは聞いたものの

これ中に入れてると具体的にどうまずくて何が起き得るんだっけ?
ベストプラクティスって言っていいの?

本題

「結局beginTransactionrollbackORcommitて、どうハンドリングすりゃ色々起きたときに問題が起きなくて開発者に優しいの?えらいひとは正しいの?」というのを整理してみます。

function hoge(PDO $connection) {
    $connection->beginTransaction(); // ①
    try {
        {クエリ発行} // ②
        $connection->commit(); // ③
    } catch(PDOException $e) {
        $connection->rollback(); // ④
        {例外時後処理}
    }
}

上記各フェーズで何が起き得るかをひたすらに考えます。

前提

  • PDOインスタンス$connection生成段階の例外・エラーの発信はないものとします。
  • PDOインスタンス生成後にDB接続に失敗するケース(処理中のネットワーク遮断など)は考えないこととします。

$connection->beginTransaction()フェーズ

想定エラーケース

参考:PDO::beginTransaction エラー/例外

  1. ドライバがトランザクションに対応していない

    • beginTransaction()でトランザクションが開始されずに失敗→catchされるとrollback()がcallされるが更に例外が起きる→try句の外に置かないと」という、冒頭の発想の理由になっているケースに思えます。
    • 確かに、このケースはこの処理が担う「クエリ発行の例外」ではないため、当処理ではなく呼び出し側でcatchするのが適正だと考えて良さそうです。
  2. トランザクションが既に開始されている

    • try外に掃き出しているので、hogeメソッド内ではcatchされず、呼び出し側でのcatchを期待する形になります。
      • あら…?このケースはtry外のcallだと却って困りますね。PDOラッパーのスコープとかで無い限りトランザクションを明示なく継続しているのはよろしくない感じがします。想定外の状態の可能性が高いので、この場合は当処理外のコミットをrollback()した上で、異常系のロギングをした方が良さそうです。
      • ドライバレベルの問題よりは、こちらの方がアプリケーション開発者によって人為的に発生しやすいケースに思えますね…

②クエリ発行フェーズ

想定エラーケース

  1. 不正なクエリ発行(構文エラーなど)
  2. deadlock、timeoutなど
    • 前提を踏まえた上で、DBレイヤで従来想定されるクエリ発行時の例外。deadlockのように、トランザクションに起因する場合もあります。

$connection->commit()フェーズ

想定エラーケース

参考:PDO::commit エラー/例外

  1. トランザクションが開始されていない
    • 今回想定の実装上はあり得ないケースです。

$connection->rollback()フェーズ

想定エラーケース

参考:PDO::rollback エラー/例外

  1. トランザクションが開始されていない
    • 今回想定の実装上はあり得ないケースです。①がtry句内で 1. のケースが発生した場合はここで再度例外が起こってしまうことになります。

結論

前提条件を踏まえた形ではありますが処理開始段階で既にトランザクションが開始されている場合に対処できていないため、ベストプラクティスではないのでは?という感じ。

どうするか?

厳密に対処するには

function hoge(PDO $connection) {
    try {
        $connection->beginTransaction();
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        try{
            $connection->rollBack();
        } catch(PDOException $re) {
            {rollback失敗時処理}
        }
        {例外時後処理}
    }
}

などとすれば、もしトランザクションが開始されていても、ドライバがトランザクション非対応でも、想定の例外ハンドリングができそうです。

が、「ドライバがトランザクションに非対応な状態」って(アプリケーションレベルの改修をしている限りでは)偶発的に発生するケースとはあまり思えないんですよね。

そう考えた場合、このケースを無視して基本的には

function hoge(PDO $connection) {
    try {
        $connection->beginTransaction();
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        $connection->rollBack();
        {例外時後処理}
    }
}

という形式で、ほとんど問題ないようにも思えます。
さよなら、わたしのなかのえらいひと・・・

まとめ

PDOトランザクション周りに関しては取るべき形がほぼ整理できたかと思います。
しかしまぁ、あまり愚直にベストプラクティスと捉えず、フレームワークやそのプロダクト自体の思想を踏まえて例外のスコープと取り回しは都度考え続けるようにしたいですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0