ダメプログラマの技術メモ

プログラミングの技術メモや駄文など

CakePHP 2.x系のbindModel+recursive=2はちょっとヤバイ?

CakePHPで3テーブル以上をbindModelで結合することってよくありますよね?

しかも、その後に、findメソッドにrecursive=2を指定することってよくありますよね?(汗

実はbindModelとrecursive>=2の組み合わせはSQLの発行回数が爆発的に増えてしまうことがあるんです。

そこで、今回はテーブル結合時に発行されるSQLの発行回数を減らす方法を述べたいと思います。


まずはサクっと今回使用するテーブルについて

3テーブル用意しました。

テーブルのレイアウトは次のとおりです。

usersテーブル

id name created modified
1 初音ミク 2012-08-18 13:24:52 2012-08-26 13:24:52

ownsテーブル

id user_id music_id created modified
1 1 1 2012-08-26 13:24:52 2012-08-26 13:24:52
2 1 2 2012-08-26 13:24:52 2012-08-26 13:24:52
3 1 3 2012-08-26 13:24:52 2012-08-26 13:24:52

musicsテーブル

id name created modified
1 みくみくにしてあげる♪【してやんよ】 2012-08-26 13:24:52 2012-08-26 13:24:52
2 メルト 2012-08-26 13:24:52 2012-08-26 13:24:52
3 ワールズエンド・ダンスホール 2012-08-26 13:24:52 2012-08-26 13:24:52

ユーザ(usersテーブル)は複数の曲を所持することが可能(ownsテーブル)で、曲の詳細情報はmusicsテーブルに入っている、という作りにしています。

リレーションはusers : owns : musics = 1 : 多 : 1というよくある構成です。

次にこの3テーブルを結合してミクちゃんが所持している曲の一覧を取得します。


ミクちゃんの所持する曲を取得するプログラム(bindModelバージョン)

public function getMusics($user_id) {
    $this->bindModel(
        array(
            'hasMany' => array('Own'),
        )
    );
    ClassRegistry::init('Own')->bindModel(
        array(
            'belongsTo' => array('Music'),
        )
    );
    $ret = $this->find('all',
        array(
            'conditions' => array(
                'User.id' => $user_id,
            ),
            'recursive' => 2,
        )
    );
    return $ret;
}

このメソッドはUser.phpにあるとします。

usersテーブルとownsテーブルは1対多、ownsテーブルとmusicsテーブルは多対1の関係にあるので、それぞれhasMany、belongsToを使用してアソシエーションの設定を行っています。

私は普段bindModelを使用することがないので間違っていたらゴメンナサイ。

何故だかわからないですが、CakePHPでは結合するときにbindModelを使用するのが一般的みたいです。

リレーションを意識してhasManyやbelongsToなどのキーワードを使い分けしなければならないので、少し面倒くさいですね。

アソシエーションの設定が完了したら、findメソッドを使用してレコードを取得するだけなのですが、この時にrecursive=2を指定するのを忘れてはいけません。

これがないとメインテーブル(usersテーブル)の孫テーブル(musicsテーブル)のレコードを引っ張ってこないので注意が必要です。


bindModel+recursive=2の場合に発行されるSQLは?

上記のfindメソッドが発行するSQLは次のようになります。

SELECT `User`.`id`, `User`.`name`, `User`.`created`, `User`.`modified` FROM `LAA0200849-cakephp`.`users` AS `User` WHERE `User`.`id` = 1
SELECT `Own`.`id`, `Own`.`user_id`, `Own`.`music_id`, `Own`.`created`, `Own`.`modified` FROM `LAA0200849-cakephp`.`owns` AS `Own` WHERE `Own`.`user_id` = (1) 
SELECT `Music`.`id`, `Music`.`name`, `Music`.`created`, `Music`.`modified` FROM `LAA0200849-cakephp`.`musics` AS `Music` WHERE `Music`.`id` = 1
SELECT `Music`.`id`, `Music`.`name`, `Music`.`created`, `Music`.`modified` FROM `LAA0200849-cakephp`.`musics` AS `Music` WHERE `Music`.`id` = 2
SELECT `Music`.`id`, `Music`.`name`, `Music`.`created`, `Music`.`modified` FROM `LAA0200849-cakephp`.`musics` AS `Music` WHERE `Music`.`id` = 3

(°д°)ハァ?

テーブル毎にSQLを発行しています。

しかも、孫テーブルに至っては、レコード毎にSQLを発行しています。

これではミクちゃんの所持する曲が増えればその分、SQLの発行回数も増えてしまいます。

こういった問題があるため、私は結合する時にはbindModelの代わりにjoinsを使用しています。


ミクちゃんの所持する曲を取得するプログラム(joinsバージョン)

public function getMusics($user_id) {
    $ret = $this->find('all',
        array(
            'fields' => array(
                'User.id', 'User.name', 'Music.name',
            ),
            'conditions' => array(
                'User.id' => $user_id,
            ),
            'joins' => array(
                array(
                    'type' => 'INNER',
                    'table' => 'owns',
                    'alias' => 'Own',
                    'conditions' => 'User.id = Own.user_id',
                ),
                array(
                    'type' => 'INNER',
                    'table' => 'musics',
                    'alias' => 'Music',
                    'conditions' => 'Own.music_id = Music.id',
                ),
            ),
        )
    );
    return $ret;
}

findメソッドの第2引数の配列の中にjoinsという項目があります。

これが結合条件を指定するためのものです。

joinsに指定できる項目は次のとおりです。

項目 意味
type 結合の種類(内部結合:INNER、左外部結合:LEFT)
table テーブル物理名
alias テーブル論理名
conditions 結合条件(aliasで指定した論理名で指定します)

サンプルプログラムを見ていただけたら、直感的に使い方がわかると思います。

bindModelよりjoinsの方が親しみやすさを感じるのは私だけでしょうか?


あと、joinsを使用する際に1つだけ注意点があります。

それはfieldsに取得カラムを指定する必要があるということです。

fieldsにカラム指定をしなかった場合は、メインテーブル(usersテーブル)のカラムしか取得しません。

まぁ、普段からfieldsに取得カラムをきちんと指定する人にとっては、全く苦にならないでしょう。


joinsを使用した場合に発行されるSQLは?

最後にjoinsを使用したfindメソッドが発行するSQLをお見せします。

SELECT `User`.`id`, `User`.`name`, `Music`.`name` FROM `LAA0200849-cakephp`.`users` AS `User` INNER JOIN `LAA0200849-cakephp`.`owns` AS `Own` ON (`User`.`id` = `Own`.`user_id`) INNER JOIN `LAA0200849-cakephp`.`musics` AS `Music` ON (`Own`.`music_id` = `Music`.`id`) WHERE `User`.`id` = 1

想定どおりのSQLになっていますね^^