トランザクションはRDBに対するひとかたまりの操作です。だから本来入れ子も何もなく、始まりと終わりが一個ずつあるだけです。以上。
…で終わらせられないのは、それでもネストが必要になる場面があるからですね。
ありがちなのは、トランザクション開始終了処理まで込みのアプリケーション側関数・メソッドやストアドプロシージャの存在。こうした関数を、まあ関数ですから部品的に扱おうとするとトランザクションが開始した文脈下でこうした関数が呼び出されて入れ子のサブトランザクションスタート、ってことになったりします。
サブトランザクションのコミットは問題ない
BEGIN TRANSACTION;
INSERT ... 'A');
BEGIN TRANSACTION;
INSERT ... 'B');
COMMIT TRANSACTION;
INSERT ... 'C');
COMMIT TRANSACTION;
内側のトランザクションがコミット終了する分には何の問題もありません。外側がコミット終了するならA,B,C全部反映されるし、外側がロールバックするなら一行も挿入されない… つまり内側のトランザクション開始・終了はなかったも同然と扱えます。
サブトランザクションのロールバックが問題
BEGIN TRANSACTION;
INSERT ... 'A');
BEGIN TRANSACTION;
INSERT ... 'B');
ROLLBACK TRANSACTION;
INSERT ... 'C');
COMMIT TRANSACTION;
問題はこれ。内側のサブトランザクションだけロールバックした場合。次のどの動作になるか。
- A,Cが入っている。
- Cだけ入っている。
- ROLLBACKの時点でエラー発生。
正解は 実装依存 。1. はなんかマイナーなRDBMSで見たことあります。PostgreSQLは 2. でした。SQL Serverは 3. らしいです。
困りましたね。こういう制御に関わる振る舞いが違ってくるとアプリケーションコードの骨格そのものがDB製品依存しかねません。
部分的ロールバックだったらSAVEPOINTがある
ストアドプロシージャや関数にトランザクション開始・終了まで入れ込んでしまう場合、処理失敗で巻き戻したいのはやっぱりその関数内で発行した変更だけです。その意味では先ほどの3択だったら 1. の動きをしてほしい。つまり部分的ロールバックです。
SQL標準はむしろ部分的ロールバックをする方法そのものはきっちり定義してくれています。それがSAVEPOINT。これを使うとこう書けます。
BEGIN TRANSACTION;
INSERT ... 'A');
SAVEPOINT SP1;
INSERT ... 'B');
ROLLBACK TO SAVEPOINT SP1;
INSERT ... 'C');
COMMIT TRANSACTION;
これで、A,Cが挿入されます。…おやQiitaさん、SAVEPOINT命令知らないの? ハイライトされてないよ? まあ、されるんです。
JavaなんかでもJDBCにしっかり setSavepoint() メソッドありますからね。使えます。
トランザクション込みの関数の書き方
SAVEPOINTの存在を考えると、部品化したい関数なら頭で単純にBEGIN TRANSACTIONというわけにはいきません。もしトランザクションの内側であれば代わりにSAVEPOINTを呼ばないといけないから。
今トランザクションの中にいるかを判定できるかというと、これは処理系次第ということになってきますが、例えばJDBCみたいに getAutoCommit() で取得できるならこれで判別するまでですね。判別するAPIが見当たらないようなら、スレッドローカル変数を使って自力管理することになります。トランザクションを開始するときにスレッドローカル変数にフラグを立てることで、それより先のスタックフレームにもわかるようにしておこうという方法。