ひさびさにPHPの話題です。
とあるフレームワークを使って書かれたソースコードを見ていて、次のような記述がありました。
try{ // トランザクション開始 // INSERT INTO … // コミット } catch(…) { // ロールバック }
このフレームワークのドキュメントにサンプルとして書かれている記述が元になっているようなのですが、これには少し違和感がありました。
トランザクションの開始自体が例外を発生させた場合には、catch内で例外が発生してしまう可能性があるわけで、まずトランザクションを開始してからtry-catchするのがスタンダードな書き方だと記憶していたわけです。
ということで、これはどういうことなのか、確認してみることにしました。
まずPDOの仕様を改めて見てみます。 http://php.net/manual/ja/pdo.transactions.php
トランザクションを使用する場合は、 PDO::beginTransaction() メソッドを使用して トランザクションを初期化する必要があります。使用しているドライバが トランザクションをサポートしていない場合は PDOException が スローされます (これは深刻な状態であるため、エラー処理の設定に かかわらず常にスローされます)。
http://php.net/manual/ja/pdo.begintransaction.php
トランザクションが既に開始されている場合や、ドライバがトランザクションに対応していない場合に PDOException をスローします。
http://php.net/manual/ja/pdo.rollback.php
有効なトランザクションがない場合に PDOException をスローします。
「トランザクションが既に開始されている場合」の例外は、先に開始したトランザクションが存在しているので、rollBack()は実行可能です。 ですので「ドライバがトランザクションに対応していない場合」を検証してみたいのですが、これは検証がかなりやっかいです。
PHPのソースを見ると、たとえばMySQLであればSTART TRANSACTION;にエラーが返ってきた場合、ことになるようです。 https://github.com/php/php-src/blob/master/ext/pdo_mysql/mysql_driver.c#L339 この状況を発生させるのは難儀ですね…。
しかたないので模式化します。 beginTransaction()でPDOExceptionが発生するということはこういうことになりますよね。
$pdo = new PDO(…); try { throw new PDOException(…); $pdo->exec(“INSERT INTO …”); $pdo->commit(); } catch(Exception $e) { $pdo->rollBack(); }
結果は次のようになりました。
Fatal error: Uncaught PDOException: There is no active transaction
予想通り、catch内のrollBack()でFatalエラーが発生することが確かめられました。
ただし上記の通り、beginTransaction()で例外発生させることは困難でした。 これは、この例外が発生する原因がプログラム側には無いからということですね。
これに対してたとえば、フレームワークの設計思想として 「プログラム側で対処できない問題が発生した場合は(主にビュー側への対処として)フレームワーク全体の『システムエラー』としてまとめる」 とするのであれば、それはそれで現実的な判断としてありなのだと思います。 今回のフレームワークではそういう設計なのだろうと推測できます。
ですので、フレームワークを使って書く側からしても、影響範囲をこの箇所に閉じこめるのか、フレームワークに投げるのか、どちらかが選択可能だと言えるわけですね。
さてでは、あくまでこの箇所に閉じこめるように書くとしたら、どう書くのがベターなのか?ということになるかと思います。
その前に少し整理しましょう。 beginTransaction()で発生する例外とrollBack()で発生する例外は同じくPDOExceptionです。 しかし、トランザクション中のSQLエラーと、トランザクションそのもののエラーは、書く側からすると意味が異なってくるのではないかと思うのですよね。
ということで、それぞれの例外についてはそれぞれ別に制御する方がベターなんじゃないかと思います。 というと、こういうことになりますでしょうか。
try { // トランザクション開始 try{ // INSERT INTO … // コミット } catch(…) { // ロールバック // 例外をスロー } } catch(…) { // エラー処理 }