CakePHPでLEFT以外もJOINしたい

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
左だっ!

この記事を三行にまとめると

全自動洗濯機が使えるのにあえて手洗いしようってなかなか思えないでしょ?
スイカ割りをやったら、どこまでも左に行かされてしまいそうですね
ドライブ行ったら、左折しかしないんでしょうね、きっと
MySQLを使い始めてもう5年くらい経ちますが、未だにJOINの細かい仕組みってのは理解できていません。LEFT JOINとRIGHT JOINの違いとか、よく分かっていません。PHPやSQLをベタ書きせず、フレームワークでの開発しかしたことがないとこうなるぞっていう、典型的な例ですね。

だからといってフレームワークを使うことを止めようとは思いませんけどね。全自動洗濯機が使えるのにあえて手洗いしようってなかなか思えないでしょ? それと同じことです。

そんなわけで、今日も今日とてCakePHPに関するお話です。



CakePHPのJOIN

CakePHPでJOINに関する処理をしたい場合、belongsToなどちょこちょこっと設定するだけで簡単に実現できます。

例えば今、ブログ記事とその記事を書いているユーザーデータがあったとしましょう。モデル名はそれぞれ「Post」「User」としときましょうか。

それぞれのデータベースの中身は、以下のようになっていると仮定します。

//postsテーブル
+----+-----------+--------+------+---------------------+
| id |  user_id  | title  | text |      created        |
+----+-----------+--------+------+---------------------+
|  1 |     1     | title1 | text | 2012-10-01 12:00:00 | 
|  2 |     2     | title2 | text | 2012-10-02 12:00:00 | 
|  3 |     1     | title3 | text | 2012-10-03 12:00:00 | 
+----+-----------+--------+------+---------------------+

//usersテーブル
+----+--------+
| id |  name  |
+----+--------+
|  1 |  name1 | 
|  2 |  name2 | 
+----+--------+

ここで、ブログ記事と書いたユーザーのデータを一緒に取ってきたい場合には、JOINを使うことで一回のSQLで取って来ることができますね。

//Post.php
class Post extends AppModel { 
  public $belongsTo = array(
    'User' => array(
      'className' => 'User',
      'foreignKey' => 'user_id',
    ),
  );
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Post.id' => 1),
    );
    $data = $this->Post->find('first', $params);
  }
}

ちょい大雑把に書いてしまいましたが、こんな感じでブログ記事と、その記事を書いたユーザーのデータを取って来れます。

このとき発行されているSQL文は、ちょっとだけ省略して書いちゃいますけど、概ねこんな感じです。

SELECT * FROM posts AS Post LEFT JOIN users AS User ON(Post.user_id = User.id) WHERE Post.id = 1

CakePHPで発行されるSELECT文は、普通はワイルドカードを使わないんですけど、全部書くのめんどいんで、今回そこは別に重要じゃないってことで、これで勘弁してつかあさい。

重要なのは、JOINのところ。「LEFT JOIN」になってますね。

このように、特に意識しなければ、CakePHPは基本的にLEFT JOINします。Cakeさんは右より左が好きなんですかね? カレイよりヒラメが好きなんですかね?

ハンター試験で右か左を選ばなきゃいけない場面に来たら、迷わず左を選んじゃうタイプなのかもしれないですね。試験管が左の法則を知っていたら、難易度の高い罠に引っかかっちゃう。

まあとにかく、CakePHPはデフォルトではLEFT JOINを使うようになっています。理由はよく分かりません。なぜなら僕はJOINのことをよく分かってないから。RIGHT JOINじゃない理由とか、よく分からん。



LEFT JOIN以外のJOINを使いたい

そんな必要があるのかないのかそれすらもよく分かりませんが、今ここで、LEFT以外のJOINもしたい。俺はRIGHTでJOINしたい。右曲がりだから右側にいる女性とJOINしたいんだぁっ!

……っていう人。そんな人にはこの言葉を捧げます。

君は何を言っているんだい?



CakePHPでRIGHT JOINを使うには、joinsっていうパラメータを使用します。

$params = array(
  'conditions' => array('Post.id' => 1),
  'joins' => array(
    'type' => 'RIGHT',
    'table' => 'users',
    'alias' => 'User',
    'conditions' => array('Post.user_id = User.id'),
  ),
);
$data = $this->Post->find('first', $params);

ちょいめんどいんですが、joinsっていうパラメータの中でJOINしたいテーブルの名前やJOINの条件を書いて、「type」でJOINの種類を指定します。RIGHT JOINしたいなら上にもあるように「’type’ => ‘RIGHT’」です。「’type’ => ‘LEFT OUTER’」だったら、LEFT OUTER JOINになります。

よく分かんないんですけど、「LEFT JOIN」と「LEFT OUTER JOIN」って、一緒なのかな?



ちなみに、モデルにbelongsToとかの設定を書いてしまっていると、このjoinsでRIGHT JOINなどは上手くうごきません。CakeさんがLEFT JOINとRIGHT JOINを同時にやろうとしてしまって、エラーになります。モデルにアソシエーションの設定が書いてある場合は、unbindModelするか、recursiveをあらかじめ-1とかにしてJOINしないようにさせておく必要があります。

あと、上記の書き方だと、Postの方のデータしか取ってきてくれません。通常のbelongsToのように、PostとUserの両方のデータを取ってきたければ、自分でfieldsを指定しないといけない。

$params = array(
  'conditions' => array('Post.id' => 1),
  'fields' => array('Post.id', 'Post.title', 'Post.text', 'User.id', 'User.name'),
  'joins' => array(
    'type' => 'RIGHT',
    'table' => 'users',
    'alias' => 'User',
    'conditions' => array('Post.user_id = User.id'),
  ),
);
$data = $this->Post->find('first', $params);

やっぱりちょい面倒ですね。あえて手間をかけさせることで、LEFT JOIN以外のものをわざわざ使う必要なんてねーんじゃねーのっていう、CakePHPからのメッセージなのかもしれません。Cakeさんと一緒にスイカ割りをやったら、どこまでも左に行かされてしまいそうですね。

カラムを特定せず、ごっそり全部取ってきたい場合は「’fields’ => array(‘*’)」でもいけます。これならSELECTのとこがワイルドカードになるので、PostとUserの全カラムのデータを取ってきます。



Containableについて

余談になりますが、CakePHPには「Containable」というビヘイビアがあります。

通常、モデルにbelongsToの設定などを書いてしまうと、必要ない場合でもJOINをしてしまうので、JOINしたくないときには自分でunbindModelを使ってJOINしないようにしなきゃいけません。それかモデルには設定を書かず、必要なときにだけbindModelを使ってJOINしないといけない。

でも毎回これを書くのはめんどい。そんな毎回毎回バインドとかしたくねえ。カルドセプトじゃあるまいし。

そんなときに重宝するのが、Containableです。

細かい説明は抜きにして、とりあえず使ってみましょう。

//Post.php
class Post extends AppModel {
  public $actsAs = array('Containable');
  public $recursive = -1;
 
  public $belongsTo = array(
    'User' => array(
      'className' => 'User',
      'foreignKey' => 'user_id',
    ),
  );
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Post.id' => 1),
      'contain' => array('User'),
    );
    $data = $this->Post->find('first', $params);
  }
}

これでオッケーです。モデルの方で「$recursive = -1」としてますが、これはデフォルトではJOINをさせないための設定です。こうやっとけば、モデルにbelongsToやらhasManyやらの設定を書いても、上記のように「contain」っていうパラメータを書かない限りJOINしない。

//Post.php
class Post extends AppModel {
  public $actsAs = array('Containable');
  public $recursive = -1;
 
  public $belongsTo = array(
    'User' => array(
      'className' => 'User',
      'foreignKey' => 'user_id',
    ),
    'Tag' => array(
      'className' => 'Tag',
      'foreignKey' => 'tag_id',
    ),
  );
}

//PostsController.php
class PostsController extends AppController {
  public function index() {
    $params = array(
      'conditions' => array('Post.id' => 1),
      'contain' => array('User'),
    );
    $data = $this->Post->find('first', $params);
  }
}

だから例えばこんな風に、UserとTagという二つのモデルとアソシエーションをしていても、「’contain’ => array(‘User’)」と書くことで、Userの方だけをbindModelした状態になるってことですね。自分でbindModelやunbindModelを書かずに済むんで、だいぶ楽になります。

ただし、残念ながらこのContainableは、LEFT JOINしかできません。Containableを使ってbelongsToをRIGHT JOINさせることはできないっぽいです。もしやりたければ、やっぱりさっきみたいに、joinsを使って書かなきゃいけないっぽいです。






ってなことで、LEFT以外のJOINのやり方でした。

アソシエーションに関してはContainableが便利だから使うと良いよって本家でも推奨されてるんですが、でもそのContainableでLEFT JOINしか使えない事実を見ると、左以外は特に必要ないだろってことなんですかね、やっぱり。

Cakeさんと一緒にドライブ行ったら、左折しかしないんでしょうね、きっと。右折とか、かたくなに拒むんでしょうね。右側に目的のお店とかあっても、何とか左折を繰り返して行くんでしょうね。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください