symfony 1.2 で Doctrine がサポートされたもののイマイチなので Propel 1.3 を使うわけだが 1.2 の Creole から 1.3 では PDO を使うようになったので transaction まわりの処理がそっちに対応して変更された.
で,PDO では「ネストした transaction」をサポートしなくて, beginTransaction() した後に再度 beginTransaction() しようとすると PDOException の例外を吐いてくださる.
Propel は Creole からの互換で,というか単にそれじゃ面倒だからだと思うが, そこをラップして多重に beginTransaction() できるよう PropelPDO クラスでオーバライドしてたりする.
でも多重 transaction をサポートしてるわけではないので微妙に挙動が直感的でなかったりする.気がする.
コードから見るルールは以下の通り.
- commit() はいちばん外側のやつだけが有効で他は無視
- rollback() はどこにあっても有効
- commit()かrollback()が働いたら transaction は終了
- transaction 終了後の commit() と rollback() は全て無視
以上のルールを念頭に……
// 普通のパターン
Propel::getConnection()->beginTransaction();
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->commit();
// $hoge->num == 1
// 多重 transaction のパターン
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->commit(); // 内側 trn の commit は無視
Propel::getConnection()->commit(); // こっちが有効
// $hoge->num == 2
ここまではいい.
// 内側 commit 外側 rollback
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->commit(); // 内側 trn の commit は無視
Propel::getConnection()->rollback(); // こっちが有効で rollback
// $hoge->num == not set
// 内側 rollback 外側 commit
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->rollback(); // こっちの rollback が有効
Propel::getConnection()->commit(); // こっちは無視
// $hoge->num == not set
外側の set 1 は内側の rollback() で消されてる点に注意.
// 内側 commit 外側 rollback その2
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->commit(); // 内側 trn の commit は無視
$hoge->setNum( 3 );
$hoge->save();
Propel::getConnection()->rollback(); // こっちが有効で rollback
// $hoge->num == not set
最後に rollback() してるから,全部なかったことに.
// 内側 rollback 外側 commit その2
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->rollback(); // こっちの rollback が有効
$hoge->setNum( 3 );
$hoge->save();
Propel::getConnection()->commit(); // こっちは無視
// $hoge->num == 3
内側の set 2 の後の rollback() で transaction が終了するので, その後の set 3 は transaction の文脈外で実行される. その後の commit も普通に無視されるので,結果 3 が残る.
// 内側 rollback 外側 rollback
Propel::getConnection()->beginTransaction(); // trn 1
$hoge->setNum( 1 );
$hoge->save();
Propel::getConnection()->beginTransaction(); // trn 2
$hoge->setNum( 2 );
$hoge->save();
Propel::getConnection()->rollback(); // こっちの rollback が有効
$hoge->setNum( 3 );
$hoge->save();
Propel::getConnection()->rollback(); // こっちは無視
// $hoge->num == 3
これも理屈は上のと同じなんだけど, 最後に rollback() してるのに set 3 が残ってるのってすごく直感的じゃないなーと.
まぁ実際には rollback() するような状況ならその後すぐ例外を throw して後の処理はさっくり飛ばすと思うんで, rollback() した後の set が動いちゃうとかって事はあまりないだろうとは思うんだけどもね.