bindModelで別のモデルを関連づけてsaveAllする際、isUniqueのバリデーションルールのせいでsave()できない場合は第2引数にfalseをセットする
タイトルの通りですがsaveAll()で保存に失敗してハマったのでメモ。
流れは以下のような感じ
- ModelA hasMany ModelB
- ModelBはModelAのキーであるmodel_a_idをもつ(外部キーがmodel_a_id)
- ModelA->saveAll()を使ってModelAとModelBの両方を同時に保存したい
以下のようなsaveAll()は通常成功する(保存データは適当)
<?php // ModelA と ModelB に保存するデータ $data = array( 'ModelA' => array( 'field' => 'value', ), 'ModelB' => array( array( 'field1' => 'value01', ), array( 'field1' => 'value11', ), ), ); // ModelAにhasManyでModelBを関連づけ $modelA = ClassRegistry::init('ModelA'); $modelA->bindModel(array( 'hasMany' => array( 'ModelB' => array( 'foreignKey' => 'model_a_id', ), ), )); // ModelA と ModelB に保存 // $modelA->id をセットしていないためinsert処理 $modelA->saveAll($data, array('validate' => 'first'));
ただ、ModelAのvalidate部分に以下のようなisUniqueバリデーションルールがあると失敗してROLLBACK。
<?php class ModelA extends AppModel { // ... public $validate = array( 'field' => array( 'isUnique' => array( 'rule' => array('isUnique'), ), ), // .... ); // ... }
SQLを確認してみるとModelBのinsert時にmodel_a_idがセットされてない。
辿ってみるとmodel.phpの1690行目や1701行目の$this->{$type}がnullになっちゃってるみたい。
$values[$this->{$type}[$association]['foreignKey']] = $this->id;
nullになっちゃってるため、foreignKeyの部分に値が入らず、そのままsave()してもforeignKeyがなくて失敗すると。
で、なんでisUniqueがあるとそうなるのかなぁと思ったら、isUniqueはテーブルをfind('count')して「行が存在しないこと」を確認しているんですよね。
findの中ではDBの問い合わせの後にresetAssociations()が呼ばれていて、これが関連を解除するってものらしい。
$results = $db->read($this, $query); $this->resetAssociations();
なので、isUniqueでバリデートを行った後に次のモデルであるModelBの処理を行おうとした時にはhasManyの情報がなくなってしまっていて失敗すると。
ここまでわかったところで「うーん、うーん、どうしたらいいんだ?」となり、
ググってみたけどイマイチ良いググりキーワードが思い浮かばず、
仕方なしに何となくCookbookを覗いていたら
// 注意: unbindModel はすぐ次の find 関数にのみ影響します。 // その次の find 呼び出しは設定済みの関連情報を使用して // 呼び出されます。
という注意文が目に飛び込んできました。
「あれ?これちょうど今知ったことじゃね?」と思いちゃんと読み進めてみたら、
もう1点。第2引数に false をセットしない限り、bindModel() や unbindModel() を使用した関連の削除や追加は、 次の モデル操作のみに作用します。第2引数が false にセットされると、bind は指定されたままの状態になります。
「おぉぉぉぉぉぉ!!!!これだぁ!!」
ということで、ちょっとソースを確認してみると
function bindModel($params, $reset = true) {
おぉぉ。確かにそれっぽい$resetって第2引数を取るようだ。
で、試しにbindModelの部分を以下のように書き換えてみた所、見事に成功しました。
$modelA->bindModel(array( 'hasMany' => array( 'ModelB' => array( 'foreignKey' => 'model_a_id', ), ), ), false); // 関連が外れないようにfalseを入れる。
教訓としては、Cookbookをたまにはちゃんと読みましょうってことですね。
あと、そもそも今回の場合はModelAのpublic $hasManyで設定しておけばbindModelを使う必要がないのでそうしとくべきでした。