読者です 読者をやめる 読者になる 読者になる

nmtysh.log

Tech系のネタや日々の独り言などを書いています。

ネストしたトランザクションに久々に嵌った

データ変更のバッチ処理を書いて、検証して「よしOK」となりローカルの環境に本適用しました。
その後、念のため同一処理を再実行しても再適用されないことを検証したら何故か適用されちゃいました。

検証中は意図的にトランザクション処理をrollback()するようにしていたのでそれが原因か? と思い調べてみても問題なし。
begin(), commit(), rollback() の呼び方がおかしいのか? と思い呼び出し方を変えてみても解決せず。

実行されたSQL文をdumpして眺めてみると何やら違和感が。
COMMIT;が実行されていない!」
SQL文の発行行数が多くてログが流れてしまったのかと思いましたが、全部ありました。

またロジック上、小分けにしてトランザクションを処理をしているので、途中でCOMMITできていないのなら無限ループに陥るはずなのですが全体処理自体は完了します。
そうすると最後の最後にCOMMITされていなくて暗黙的にROLLBACKされている? と思い、処理の最後にCOMMIT文を追加しました。
すると処理後のデータがDBに残りました。

このタイミングでトランザクション処理のスコープを確認して見ると、ネストしたトランザクションになっていることが判りました。

<?php
function execute() {
    $this->Model->begin(); // begin(a)
    /* 処理対象の抽出処理 */
    if () {
        // 処理対象なし
        return true; // (1)
    }
    // 一定件数ずつ取得して処理。全部処理が終わった完了。
    $records = $this->Model->find('all');
    do {
        foreach ($records as $record) {
            // 処理
            if () {
                // 失敗
                $this->Model->rollback(); // rollback(a)
                return false;
            }
        }
        $this->Model->commit(); // commit(a)
        $this->Model->begin(); // begin(b)
        $records = $this->Model->find('all');
    } while(!empty($records));
    return true; // (2)
}

だいぶ端折ったコードですが大体こんな感じです。
このコードの問題点は、2つあります。

  1. 処理対象が無かった時にreturn(1)で処理を抜けていますが、begin(a)で開始したトランザクションが終了していません。
  2. 処理が完了した時のreturn(2)ではdo...while文の中で開始されるbegin(b)で開始したトランザクションが終了していません。
    ※begin(a)はdo...while文内のrollback(a)かcommit(a)で終了します。whileがループした場合は前回のループで開始したbegin(b)が終了します。

CakePHPの場合デフォルトだとネストしたトランザクションは利用できないため、内側のトランザクション発行、終了は無視されます。(そのように解釈しました間違っていたら教えて下さい)
そのため、この処理だと一番外側のトランザクションが終了していないので、このプログラム全体の処理が終了した時点でトランザクションが破棄されすべてrollbackされます。

begin()とrollback()/commit()はどの分岐でも対になるように注意します。

ネストできないのは知っていたので、commit()したらそれまでに何度begin()が呼ばれていても関係なくすべてcommit()されると勘違いしていました(rollbackも同様になると思っていました)
まさか内側のトランザクション発行は無視されるとは……

関連