目次
PDOのcommit()・rollback()が例外をスローする/PHP7とPHP8の非互換
DBはMariaDBです(10.3)。
PDOを使ったPHP7.動いていたシステムを、PHP8にバージョンアップしたら、登録処理のいくつかが例外でおちるようになったと連絡がありました。
まだテスト段階なのですが、原因がわからないとヘルプを頼まれました。
その時の記録です。
PDOのエラーモードのデフォルト変更かと思ったが違った
最初は、PDOのエラーモードのデフォルトが変わったからだと思いました。
PHP 8.0.0 より前は、PDO::ERRMODE_SILENTが、デフォルトのモードでした。
つまり、例外を発生させないダンマリモードです。
PHP8.0.0以降は、PDO::ERRMODE_EXCEPTIONがデフォルトのモードになりました。
つまり、例外を発生させるモードです。
もし、PDO::ATTR_ERRMODEを変更せず、デフォルトで使っていたとしたら、エラーのある処理で今まで発生していなかった例外がスローされるようになるのは、当たり前なので、確認したのですが。
さすがに、それはなくPHP7でもちゃんとPDO::ERRMODE_EXCEPTIONを設定してつかってました。
なので、これは原因ではありません。
例外にならないトランザクションとならないものがある
問題があるといわれる処理をデバッガで追いかけてみると、確かに、PDO::commit()とrollback()で例外()が発生しています。
でも、すべてが同じようにエラーになるわけではありません。
正常終了するトランザクションもあります。
PHP7の時はすべてが正常に動いていた(ように見えていた)トランザクション処理で、PHP8になるとCommit()・rollback()が例外になるものと、ならないものがある。
この違いが何かということですね。
いくつか比較すると、ある共通メソッドをトランザクション中によんでいるかどうかの違いがあるのがわかりました。
それで、その共通メソッドを調べてみると・・ありました。
メソッドの中で「truncate table」を使ってます。
こいつがくさいです。
例外になるトランザクションには暗黙的なコミットが関係している
なぜ「truncate table」がくさいかというと。
トランザクション中でも、「truncate table」が発行されると、その時点で暗黙駅なコミット(Implicit Commit)が発生してしまうからです。
上記の記事に、暗黙のコミットが発生するステートメントのリストがのってますが、しっかり「truncate table」がのっています。
この暗黙のコミットははいる位置によっては、データの不整合を引き起こしかねないので、CREATE TABLE や DROP TABLEなんかを「TEMPORARY」をつけて使う以外のDDL文はトランザクションの中にかかないほうがいいということは、ある程度周知だったのですが、外部メソッドの中なので油断した感じですかね。
暗黙のコミットでPDO::inTransaction() がfalseを返してるのが原因
暗黙のコミットがあっても、PHP7の時にはcommit()でエラーにはなりません。
でも、PHP8では厳しくなってます。
下位互換性のない変更に書かれています。
引用します。
PDO MySQL
PDO::inTransaction() は、 PDO が管理しているおおよその情報ではなく、 実際のトランザクションの状態を報告するようになりました。 クエリが "暗黙のコミット" に依存していた場合、 PDO::inTransaction() は後に false を返します。 なぜなら、トランザクションが既にアクティブではないからです。
これを見る限り、PHP8の仕様変更によって「暗黙のコミットが発生したトランザクションは、PDO::inTransaction() がfalseをかえすようになったため、commit()・rollback()が例外をスローするようになった」と解釈したほうがよさげです。
とりあえず、そのように報告しました。
反則技かもしれないと思いつつ、修正の時間稼ぎに考えた回避策
実際、うまくいってるトランザクションもあるわけなので、暗黙のコミットがおきないようにDDL文が意図せず含まれてしまっているトランザクションを修正すればよい・・と回答しておしまいにしようとしてました。
正直、トランザクションの中にDDL文を書いているようなことが、それほどあるわけがないと思ったので。
ところが。
思ったより影響があって、修正するには時間がかかる。
でも、登録処理ができないと、テストがすすめられなくてスケジュールが遅れるので、一時的になんとかできないかと頼まれました。
それで着目したのが、前に引用した「PDO::inTransaction() は、 PDO が管理しているおおよその情報ではなく」の部分。
この「PDOが管理しているおおよその情報」というのは「PDO::errorCode()」の返す値のはずなので、試しに、commit()で例外が発生したときの値を調べてみると「00000」つまり「正常なSQLである」という結果がかえっています。
これを利用することにしました。
幸い、一応commit()はfunctionでラップしてあったので、そこに「PDO::errorCode()」の返す値が「00000」だった時には、例外が発生してもTrueを返すようにしました。
public function trn_commit() { try { $ret = $this->commit(); if($ret === true) { return true; } else { return false; } } catch(PDOException $e) { $chk = $this->errorCode(); if(strcmp($chk, "00000") == 0){ return true; } return false; } }
とりあえず、これでそれまで通り登録できることは確認できたので、よしとします。
まあ、一時しのぎなので、これで時間を稼いでいる間に、暗黙のコミットが発生しないようにプログラムのトランザクション処理の見直しをすすめてもらえればと思います。
いろいろありますね。
ではでは。