PDOでトランザクションを開始する場合の順序


ひさびさに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(…) { // エラー処理 }

最新記事

すべて表示

SQLite(sqlite3)で “no such table”

小ネタです。 SQLiteを使っていて "no such table" とエラーが出た場合、 DBファイル名の指定が空になっている、という凡ミスを起こしていないかを確認してみましょう。 ・・・ そういう凡ミスをしてしばし悩んだので… ファイル名の指定が空になっている場合、一時的なインメモリDBとして保存されます(※1)。 つまりDB接続を切断すると中身は消えます。 なので接続

アプリケーションサーバにポートを指定せずに起動すると?

最近、 Goで書かれたアプリケーションサーバが起動しない! ->原因: .env ファイルが欠けていた というドタバタがありました。 結局Goと関係ないですが、この時、 「あまりGoに慣れてないのでGoの問題かと…」「DockerまだよくわかってなくてDockerの問題かと…」 というような声があったのて、あえてGoで検証してみようと思ったわけです。 さて、Goでサーバサイドのシステムを作

GitLab 9.1.2 (MySQL) を 11.4.0 (PostgreSQL) にアップグレード

弊社ではかなり前からGitLab(CE)を自社環境で運用しているのですが、ふと気付くと、バージョンがだいぶ先に行ってしまっていました。 とくに最近のバージョンでは Auto DevOps なども使えるようになっていたりするので、さすがにそろそろキャッチアップしたいと考えたわけです。 現行の環境は次の通りです: GitLab 9.1.2 sameersbn/gitlab 使用 MySQL 5.6