CakePHP 1.3でのトランザクション処理の方法と注意点

CakePHP 1.3でのModel::saveAll()を利用したトランザクション処理において、保存しているはずのデータが上手く保存されず、かなりハマってしまったのでメモしておきます。

環境

  • CakePHP 1.3.2を使用。
  • データベースはMySQL。ストレージエンジンはInnoDB。

処理内容

  1. モデル「ModelA」のデータをModelA::save()で更新。
  2. モデル「ModelB」のデータをhasManyの関連を持つデータとともにModelB::saveAll()で作成(トランザクション処理)。
  3. (モデル「ModelB」のデータの作成に失敗した場合はエラー処理後に処理を継続。)
  4. モデル「ModelA」のデータをModelA::save()で更新。
$data = array(...);
$this->ModelA->create(null); $this->ModelA->set($data);
$this->ModelA->save();
...
$data = array(...);
if (!$this->ModelB->saveAll($data)) {
    エラー処理
}
...
$data = array(...);
$this->ModelA->create(null); $this->ModelA->set($data);
$this->ModelA->save();

期待した結果

ModelB::saveAll()の成否に関わらず、前後のModelA::save()でModelAのデータが更新される。

実際の結果

ModelB::saveAll()のバリデーションに失敗すると、ModelA::save()がSQLステートメントを実行し、戻り値として配列(更新に成功)を返すにも関わらず、最後のModelA::save()で更新したデータのみが反映されない。

原因

Model::saveAll()の第2引数を指定しない場合、Model::saveAll()は以下のような処理を行っています。

  1. トランザクションを開始(データソースがトランザクションに対応している場合)。
  2. データの検証(バリデーション)。
  3. データの保存。
  4. トランザクションをコミット、あるいはロールバック(データソースがトランザクションに対応している場合)。

バリデーションに成功した場合は問題ないものの、バリデーションに失敗した場合はコミットもロールバックもされず、トランザクションが開始された状態のままになるようです。

今回の例では、ModelB::saveAll()でのバリデーションを失敗させるケースがあり、その場合にModelA::save()の処理がトランザクションの一部として扱われ、その結果、更新しているはずのデータが反映されなかったようです。

解決策

色々と調査してみた範囲では以下の解決策が取れそうです。

Model::saveAll()の第2引数でarray('validate' => ...)を指定

Model::saveAll()の処理をバリデーションと作成に分ける方法です。バリデーションに成功した場合のみデータを保存するようにします。

$data = array(...);
if ($this->ModelB->saveAll($data, array('validate' => 'only'))) {
    $this->ModelB->saveAll($data, array('validate' => false));
} else {
    エラー処理
}

array('validate' => 'only')の場合、Model::saveAll()はバリデーションのみを行います。array('validate' => false)の場合は、Model::saveAll()はバリデーションなしにデータを保存しようとします。デフォルト値はarray('validate' => 'first')でバリデーション後にデータを保存しようとします。

成否は真偽値で返ります。ただし、array('validate' => false)で失敗した場合はNULLが返る???

Model::saveAll()の第2引数でarray('atomic' => false)を指定

Model::saveAll()でのトランザクション処理を無効化して、自前でトランザクションの開始・コミット・ロールバックを行う方法です。モデルを拡張して、begin()、commit()、rollback()を利用できるようにします。

/app/models/app_model.php
class AppModel extends Model {
    public function begin() {
        return $this->getDataSource()->begin($this);
    }

    public function commit() {
        return $this->getDataSource()->commit($this);
    }

    public function rollback() {
        return $this->getDataSource()->rollback($this);
    }
}
$data = array(...);
$this->ModelB->begin();
$results = $this->ModelB->saveAll($data, array('atomic' => false));
戻り値の処理
if (保存に成功) {
    $this->ModelB->commit();
} else {
    エラー処理
    $this->ModelB->rollback();
}

array('atomic' => false)の場合、Model::saveAll()はレコードごとに保存を行おうとします。成否は、モデルのエイリアス(モデル名ではない)をキー、真偽値を値とした以下のような配列で返ります。ただし、失敗した場合はNULLが返る???

array(2) {
  ["ModelA"] =>
  bool(true)
  ["ModelB"] =>
  bool(true)
}

hasManyの関連があるモデルなどをarray('atomic' => false)を指定して保存しようとした場合、戻り値は以下のような配列になります。

array(2) {
  ["ModelA"] =>
  bool(true)
  ["ModelB"] =>
  array(2) {
    [0]=>
    bool(true)
    [1]=>
    bool(true)
  }
}

デフォルト値はarray('atomic' => true)で、Model::saveAll()は複数レコードの保存を単一のトランザクションとして行おうとします(トランザクションに対応している場合)。成否は真偽値で返ります。ただし、失敗した場合はNULLが返る???

今回は、他にトランザクション処理が必要な箇所もなく、処理が簡単ということもあり、バリデーションと保存を分ける方法で対処しました。トランザクション処理が多い場合には、モデルクラスを拡張するなり、その都度、自前で処理したほうがわかりやすく、変にハマってしまうこともないかもしれません。

Model::saveAll()の戻り値はAPIドキュメントを参考にしましたが、第2引数の組み合わせによって、実際の戻り値の形式が変わってくるようです。戻り値のパターンが複雑なようなので、Model::saveAll()を利用する場合には戻り値にも注意しておいた方が良さそうです。

コメント (0)

この記事へのコメントはまだありません。

コメントフォーム

トラックバック (1)

[…] CakePHP 1.3でのトランザクション処理の方法と注意点 – (DxD)∞ ↑全てのモデルでやる必要はないらしい […]

この記事のトラックバックURI
http://dxd8.com/archives/211/trackback/
この記事のURI
http://dxd8.com/archives/211/