cakephp 1.3.3で開発しております。
今回は先日わたくしがハマった事象についてお話します。多少書きなぐり気味ですが><許してください。
前提条件
1.A_controllerでpaginateしている
2.paginate対象のモデル(User)には色々なモデルが関連付けられているが、その1部であるHogehogeというモデルをLEFTJOINではなく、INNERJOINで連結したいので一度unbindする
3.このメソッドが呼ばれる前にapp_controllerのbeforeFilter()にUserモデルで検索をかけている箇所がある。そこはContainableBehaviorが使われている
という前提でした。
1.A_controllerのpaginateの箇所はこんなかんじ。
$this->User->unbindModel(array('belongsTo'=>array('Hogehoge')), false);//第2引数をfalseにすることでモデルの関連性を維持させる
'joins'=>array(
array(
'type' =>'INNER',
'fields'=>array('Hogehoge.name'),
'table'=>'`Hogehoges`',
'alias'=>'`Hogehoge`',
'conditions'=>array(
'Hogehoge.id = Hogehoge.user_id',
),
),
),
//このあとpaginate実行
でハッピー! にならなかった・・・というお話。
まず、unbindModelの第2引数にfalseを渡すことで永続的にモデルのアソシエーション状態を保持してる、つもり。
ブログ記事でよく見かけますが、ここにfalseを設定しないと、paginateでは
・件数
・欲しいレコード
を段階的に取得するため、2回目のクエリでアソシエーションがもどってしまうという。という記事がじゃんじゃん、でてくる。
だがしかし。
falseにしてるのにどーやってもあーやってもHogehogeテーブルがLEFTJOINされ、上記のINNERJOINのHogehogeテーブルまでくっついてくるので「 Not unique table/alias: ‘Hogehoge’ 」って怒られます。憤ります。ぷるぷる。別名とか別にLEFTJOINでもいいなとか色々選択肢はあったのですが、最近「ソースコードよめよ」なので一念発起してソースの旅に出てみることにしました。cakeフォルダのなかにダイヴします。
まずコアソースのcontroller.phpでpaginate()をあさります。ださいこと極まりないですが、ここでかたっぱしからdebug($object->belongsTo);を出力して、どうやら
$count = $object->find('count', array_merge($parameters, $extra));
ここの前後で、belongsToの中身が、もともとのモデルにセットされているアソシエーションにもどってしまうことが分りました。つまり、falseにしているにも関わらず件数取得時にアソシエーションがリセットされていることになります。なので、こんどはmodel::find()をあさります。
すると、今度はmodel::findにある
$results = $this->__filterResults($results);
この箇所でbelongsToの内容が変化してしまうことがわかりました。そんな感じでソースをぐるぐるを追いかけていったところ結局、ここではcallback関数がよばれていて、model_behavior.phpにいき、
$this->Behaviors->trigger($this, 'afterFind', array($results, $primary), array('modParams' => true));
のようなtriggerメソッドから、afterFind()がよばれていることに。結論からいうと、app_controller.phpで
3。のメソッドが実行される
→ここで、Userモデルの変数に__backAssociationという、いわば「モデルの既存アソシエーション」がバックアップのような意味合いでセットされる。
次に、paginate処理を行う
→ここで、paginateのINNERJOINを実行したいためunbindする(INNER JOIN対象のモデルを、Userモデルから切り離す)
→paginate実行
内部処理:カウントを取得 →ここで、カウントを取得。この時点はアソシエーションはunbind状態が保持されている
→count取得時のfindのafterFindが実行。その際、既にUserモデルにセットされている__backAssociationの値を参照しておりここに値が入っていると明示的にモデルのアソシエーションをデフォルトに戻してしまう
→実際のレコードを検出(findが実行)
→既にアソシエーションがもどっているため、unbindされたモデルなんぞは無視され、同じモデルが2回JOINされる有様。
なんで、model_behavior.phpのafterFindなのかというと、app_controller側で呼び出したメソッド内でUserモデルにビヘイビアがセットされるため、paginateではBehaviorは使っていないにもかかわらずここのafterFindが呼ばれる・・・らしい。ここだけはちょっとあやふや。もし「ここおかしいよ」って思われる方がいらっしゃったらご指摘いただけると。
対処方法:app_conrtollerのbeforeFilter()のクエリをContainableBehaviorを使わずに普通にjoinした。
とりあえずこの対応方法で行ったのですが…根本的ではないですね。
そもそも、事前に実行されるメソッドで利用したモデルの構成は引き継がれるという当たり前なことに気が付かず、 app_controllerにこのメソッドを追加する前まではpaginate処理が動作していたので非常にデバッグに時間がかかってしまいました。
一度Userモデルを使っているわけですから、そういう意味では原因がわかって「そりゃそうだな・・・」という気持ちになったのも事実です・・が
事前にAモデルをContainableBehaviorを使って実行し、そのまま続けてAモデルのモデル構成を変更(bind/unbind)しpaginateを行った場合に第2引数のfalseは役に立たない という結論で・・・よろしいか?
こういう状況って皆さんあまりない・・かな。不具合とはいえないかもなんですが、ちょっと気になったのでネタにしてみました。 鼻の黒い人にこんな記事を見つけてもらったのですが同じ事をいってる・・かな。そう考えると、同じモデルのメソッドを別々のところで呼び出しているのもどうなのかなーって事もあるのかもですが、
ContainableBehaviorとpaginateの相性はそこまでウフフな関係にはいたっていないということでしょうか。
アソシエーションを密接にしておくと、思わぬところで意識していなかったモデルの姿形がかわっとる!という事があるかもしれないですね・・