久しぶりにcakePHPの記事。

cakePHPではアソシエーションという、テーブルとテーブルの関連付け機能を核にもっていて、なかなか便利です。

  • 「ユーザー(User)」は「プロフィール(Profile)」を一つだけ持つ
  • 「ユーザー(User)」はお気に入りの「曲(Music)」をたくさん持つ
  • 「ユーザー(User)」はお気に入りの「アーティスト(Artist)」をたくさん持つ
  • 「ユーザー(User)」はお気に入りの「映画(Movie)」をたくさん持つ
  • 「曲(Music)」は提供する「アーティスト(Artist)」を一つだけ持つ

このような構造を設定できるので、「ユーザー(User)」情報を取得した時に、関連する「プロフィール(Profile)」、「曲(Music)」、「アーティスト(Artist)」、「映画(Movie)」さらには曲に関連する「アーティスト(Artist)」のデータまでまとめて取ってくるということが簡単にできます。これがアソシエーションです。

しかし、このアソシエーション。
簡単に設定できるのですが、設定してしまうとユーザー情報と曲のデータだけ欲しいというときにも、アーティストや映画の情報まで引っ付いてきてしまいます。おまけに、不要なアーティストや映画の全フィールドがくっついてきます。

「今回はアーティストと、映画のデータはいらないから」と、関連を一個一個削除することもできますが、ContainableBehaviorを使うと、「曲のデータだけいるよ」と設定すればその他の余計なものが勝手に除外されるので直観的だし楽です。
さらに、フィールドのフィルタリング(曲データの中でも、「曲名」だけ取ってくるなど)も簡単に扱えます。

※ContainableBehaviorはあくまでアソシエーションに対してフィルタリングをするので、アソシエーションの設定と置き換わるものではありません。

ContainableBehaviorを使えるようにする

ContainableBehaviorを使うためには、利用したいモデルに以下を追記します。

class User extends AppModel {
    var $actsAs = array('Containable');
}

今回の説明で使うUser, Musicモデルは、こんな風になっています。あとのモデルは説明にいらないので割愛しています。
(Containableの利用宣言は、主体になるモデルにあればいいみたいです。)

/* User.php */
class User extends AppModel {
    var $actsAs = array('Containable');
    public $hasOne = array('Profile');
    public $hasMany = array('Music','Artist','Movie');
}

/* Music.php */
class Music extends AppModel {
    public $belongsTo = array('Artist');
}

ContainableBehaviorの使い方

ContainableBehaviorの利用を宣言すると、find()を使うときに、「contain」を指定できるようになります。

以下の例では、User.id=5に該当するユーザーのProfileの全フィールド、Musicの曲名と提供アーティスト名(外部キーでartist_idを持っているとする)、お気に入りアーティストArtistのアーティスト名、映画Movieの映画名を取得しています。

/* UsersController.php */

// findを呼び出す箇所
$option= array(
    'conditions' => array(
        'User.id' => 5
    ),
    'contain' => array(
        'Profile',
        'Music.music_name',
        'Music' => array(
            'Artist.artist_disp_name'
        ),
        'Artist.artist_disp_name',
        'Movie.movie_name'
    )
);
$this->User->find('all', $option);

Musicの箇所のように、外部キーをたどって階層の深いデータを取ってくるときも簡単ですね。便利。
各関連データに、フィルタ条件(conditions)や並べ替え(order)を指定することもできます。

$option= array(
    'conditions' => array(
        'User.id' => 5
    ),
    'contain' => array(
        'Music' => array(
            'conditions' => array('Music.delete_flg' => 0),
            'order' => 'Music.name ASC'
        )
    )
);
$this->User->find('all', $option);

conditionsやorderを指定する時は若干書き方が変わるので注意ですね。
この場合は、User.id=5のユーザーの、お気に入り曲でMusic.delete_flg=0の曲名リストが取得できます。
アソシエーションが設定されていても、ArtistやMovieの情報は除外されます。
不要なデータが除外されることで、アソシエーションの関連データが全て読み込まれる場合に比べてパフォーマンスアップにもなります。

contain指定する時の注意

contain指定だから、というわけではないのですが、アソシエーションによるデータ取得の時、ソート(order)が思ったようにならない、ということがあります。

上の例では、Music.nameでorderを設定していますね。この場合、User.id=5のユーザーが登録したお気に入り曲の一覧が曲名の昇順で取得できます。そういう場合は特に問題ないでしょう。

ただ、データベースを真面目に正規化していると、テーブルAとテーブルBを結合して、テーブルAとテーブルBの結合データを、テーブルBのフィールドで並べ替えしたいなぁというような場合が割とあるんじゃないでしょうか。(A left join BしてB.numで並べ替えたい、みたいな。)

アソシエーションの、belongsToはleft joinになるのでその関係にあるテーブルならこれができますが、hasManyの場合は結合ではなく別のSELECT文が発行されるので、この並び替えができないのです。

なんとなく全部くっついたデータが取得できてるイメージでいたので、どうやっても並び替えできなくて困りました。
デバッグで発行されたSQLを見てみると、謎がとけます…。
ふつうに使う分には便利なんですが、ちょっと注意が必要ですね。

おわり