先輩のamo-kさんから、「Symfonyのトランザクション処理時の動作について調べてみて」と言われたので、Symfonyの勉強がてら調べてみました。 Synfony のログからわかること まず Symfony のログを見てみると、begin -> begin -> category テーブルの update -> commit -> begin -> begin -> bookmark テーブルの update -> commit -> commit という処理の流れになっています。 アクションで明示的に記述したトランザクションとは別に、$category->save では1重、$bookmark->save では2重のトランザクションが走っているように見えます。 モデルクラスの中身を見てみる $bookmark と $category はそれぞれ Symfony のスクリプトによって自動生成されたモデルクラスである BookMark と Category のインスタンスです。 Bookmark と Category はそれぞれ BaseBookmark と BaseCategory を継承しており、実際の更新処理はこれらのベースクラスに記述されています。 save関数の中でもトランザクションが使われています。 BaseBookmark と BaseCategory の save 関数内に以下のような記述があります。
// BaseBookmark::save 関数の一部(BaseBookmark.php)、BaseCategory::save (BaseCategory.php)も同様
$con->begin();
$affectedRows = $this->doSave($con);
$con->commit();
return $affectedRows;
これを見てわかるとおり、 save 関数内でも $con->begin() ~ $con->commit() によって、トランザクション処理を行っています。 doSave 関数の中では、このインスタンスが新規のレコードなのか、レコードの更新なのかを判定し、新規であれば Peer クラスの doInsert を、更新であれば doUpdate を呼び出すようになっています。 以下、該当部分のソースコード。
// BaseBookmark::doSave 関数の一部(BaseBookmark.php)
  if ($this->isNew()) {
    $pk = BookmarkPeer::doInsert($this, $con);
    $affectedRows += 1;
    $this->setId($pk);
    $this->setNew(false);
  } else {
    $affectedRows += CategoryPeer::doUpdate($this, $con);
  }
doInsert 関数と doUpdate 関数は BookmarkPeer の親クラスである BaseBookmarkPeer クラスで定義されていています。 doInsert関数の中では、以下のように $con->begin() でトランザクションを開始したあと、BasePeer クラスの doInsert 関数を呼び出しています。これが、 $bookmark->save() を実行したときの、最も深い begin ~ commit の正体です。
// BaseBookmarkPeer::doInsert 関数の一部(BaseBookmark.php)
  try {
    $con->begin();
    $pk = BasePeer::doInsert($criteria, $con);
    $con->commit();
  } catch(PropelException $e) {
    $con->rollback();
    throw $e;
  }
なお、doUpdate 関数内ではトランザクションをかけていません。アクション内で記述していた $category->save() はすでにあるレコードの更新操作なので doUpdate が実行され、該当部分のトランザクションは2重までになります。 コネクションオブジェクトに対する関数呼び出しのログ出力処理 アプリケーションをデバッグモードで実行しているときは $con は sfDebugConnection というクラスのインスタンスになっていて、各モデルクラスはこのインスタンスを通して、DB操作を行うようになっています。 このクラスはsymfonyで提供されているもので、コネクションオブジェクトに対して実行された処理のログをとっています。例えば、commit 関数なら以下のようになっています。
 // sfDebugConnection.php から抜粋
  public function commit()
  {
    $this->log("{sfCreole} committing transaction.");
    return $this->childConnection->commit();
  }
関数が呼び出されると無条件にログ出力が行われます。そのため、今回のようにコネクションオブジェクトに対して最大で3重のトランザクションをかけている場合は、ログにもそのように出力されるわけです。 その次に $this->childConnection の同名の関数を呼び出すという処理になっています。 $this->childConnection は実際に使われているDBに応じた、Connectionインタフェースを実装したドライバになります。今回はMySQLを使用しているので、MySQLConnectionというクラスになっています。 MySQLConnection は ConnectionCommon というクラスを継承しています。DBの実装に関係なくコネクションで共通の処理が ConnectionCommon に記述されています。 begin関数、commit関数はConnectionCommonで以下のように定義されています。
// sfConnectionCommon.php から抜粋
    public function begin()
    {
        if ($this->transactionOpcount === 0 || $this->supportsNestedTrans()) {
            $this->beginTrans();
        }
        $this->transactionOpcount++;
    }

    public function commit()
    {
        if ($this->transactionOpcount > 0) {
            if ($this->transactionOpcount == 1 || $this->supportsNestedTrans()) {
                $this->commitTrans();
            }
            $this->transactionOpcount--;
        }
    }
begin関数で transactionOpcount を +1 し、commit関数で -1 するようになっています。 ここで supportNestedTrans 関数はDBがトランザクションの入れ子をサポートしている場合にtrueを返します。今回使用しているMySQLはトランザクションの入れ子をサポートしないので、この関数は false を返します。 ConnectionCommon のサブクラスで beginTrans および commitTrans 関数がオーバーライドされ、実際のトランザクション開始~コミット処理を行っています。 この部分の処理で、MySQLのようにトランザクションの入れ子をサポートしていないDBを使用している場合は最も外側のトランザクションのみが有効になることがわかります。 以上の処理をシーケンス図にすると、以下のようになります。


トランザクション処理時のシーケンス(拡大画像へリンク) と、ここまでわかったところで、この記事を発見。まさに知りたかったことが書いてあるじゃないか・・・。 まあ、もう少し詳細にコードを追っているので、本記事も参考になるのではないでしょうか。