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

# 昔えらいひとが言ってた気がする
PDO使っててtransaction張る時、

```php
function hoge(PDO $connection) {
    try {
        $connection->beginTransaction();
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        $connection->rollBack();
        {例外時後処理}
    }
}
```
諸所の処理に紛れてたまにこんな構造になったりしてませんか？
これは厳密なケースに対応しきれずよろしくありません。

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

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

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

# 本題

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

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

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

## 前提 

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

## ①`$connection->beginTransaction()`フェーズ

###想定エラーケース
参考：[PDO::beginTransaction エラー/例外](https://www.php.net/manual/ja/pdo.begintransaction.php#refsect1-pdo.begintransaction-errors)

1. ドライバがトランザクションに対応していない
  * 「`beginTransaction()`でトランザクションが開始されずに失敗→catchされると`rollback()`がcallされるが更に例外が起きる→try句の外に置かないと」という、冒頭の発想の理由になっているケースに思えます。
  * 確かに、このケースは**この処理が担う「クエリ発行の例外」ではない**ため、当処理ではなく呼び出し側でcatchするのが適正だと考えて良さそうです。
 
1. トランザクションが既に開始されている
  * try外に掃き出しているので、`hoge`メソッド内ではcatchされず、呼び出し側でのcatchを期待する形になります。
     * あら…？このケースは**try外のcallだと却って困ります**ね。PDOラッパーのスコープとかで無い限りトランザクションを明示なく継続しているのはよろしくない感じがします。想定外の状態の可能性が高いので、この場合は当処理外のコミットを`rollback()`した上で、異常系のロギングをした方が良さそうです。
     * ドライバレベルの問題よりは、こちらの方が**アプリケーション開発者によって人為的に発生しやすいケース**に思えますね…

## ②クエリ発行フェーズ

###想定エラーケース

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

## ③`$connection->commit()`フェーズ

###想定エラーケース
参考：[PDO::commit エラー/例外](https://www.php.net/manual/ja/pdo.commit.php#refsect1-pdo.commit-errors)

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

## ④`$connection->rollback()`フェーズ

###想定エラーケース
参考：[PDO::rollback エラー/例外](https://www.php.net/manual/ja/pdo.rollback.php#refsect1-pdo.rollback-errors)

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

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

## どうするか？
厳密に対処するには

```php
function hoge(PDO $connection) {
    try {
        $connection->beginTransaction();
        {クエリ発行}
        $connection->commit();
    } catch(PDOException $e) {
        try{
            $connection->rollBack();
        } catch(PDOException $re) {
            {rollback失敗時処理}
        }
        {例外時後処理}
    }
}
```
などとすれば、もしトランザクションが開始されていても、ドライバがトランザクション非対応でも、想定の例外ハンドリングができそうです。

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

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

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

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

#まとめ

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