bindModelで別のモデルを関連づけてsaveAllする際、isUniqueのバリデーションルールのせいでsave()できない場合は第2引数にfalseをセットする

タイトルの通りですがsaveAll()で保存に失敗してハマったのでメモ。



流れは以下のような感じ

  1. ModelA hasMany ModelB
  2. ModelBはModelAのキーであるmodel_a_idをもつ(外部キーがmodel_a_id)
  3. 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を使う必要がないのでそうしとくべきでした。