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になっていますね^^