CakePHP 1.3でのトランザクション処理の方法と注意点
CakePHP 1.3でのModel::saveAll()を利用したトランザクション処理において、保存しているはずのデータが上手く保存されず、かなりハマってしまったのでメモしておきます。
環境
- CakePHP 1.3.2を使用。
- データベースはMySQL。ストレージエンジンはInnoDB。
処理内容
- モデル「ModelA」のデータをModelA::save()で更新。
- モデル「ModelB」のデータをhasManyの関連を持つデータとともにModelB::saveAll()で作成(トランザクション処理)。
- (モデル「ModelB」のデータの作成に失敗した場合はエラー処理後に処理を継続。)
- モデル「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()は以下のような処理を行っています。
- トランザクションを開始(データソースがトランザクションに対応している場合)。
- データの検証(バリデーション)。
- データの保存。
- トランザクションをコミット、あるいはロールバック(データソースがトランザクションに対応している場合)。
バリデーションに成功した場合は問題ないものの、バリデーションに失敗した場合はコミットもロールバックもされず、トランザクションが開始された状態のままになるようです。
今回の例では、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()を利用する場合には戻り値にも注意しておいた方が良さそうです。
トラックバック (1)
[…] CakePHP 1.3でのトランザクション処理の方法と注意点 – (DxD)∞ ↑全てのモデルでやる必要はないらしい […]
コメント (0)
この記事へのコメントはまだありません。