Yiiで中間テーブルに属性を持たせたいときはHasMany, BelongsToを使う
YiiFrameworkのCManyManyRelationは中間テーブルへのアクセス手段を用意してくれません。なので、中間テーブルに属性を持たせたいときはHasManyとBelongsToを使うのが、今のところエレガントな解決方法のようです。
http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c6007
http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c7483
この問題のサンプルコードにはtypoがたくさんあるので、検証コードを載せておきます。
テーブル
CREATE TABLE IF NOT EXISTS `viewer` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `movie` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `viewer_watched_movie` ( `id` int(11) NOT NULL AUTO_INCREMENT, `viewer_id` int(11) NOT NULL, `movie_id` int(11) NOT NULL, `liked` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_viewer_watched` (`movie_id`), KEY `fk_movie_watched_by` (`viewer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
データ
INSERT INTO `viewer` (`id`, `name`) VALUES (1, 'v1'), (2, 'v2'); INSERT INTO `movie` (`id`, `title`) VALUES (1, 'm1'), (2, 'm2'); INSERT INTO `viewer_watched_movie` (`id`, `viewer_id`, `movie_id`, `liked`) VALUES (1, 1, 1, 1), (2, 1, 2, 0), (3, 2, 1, 1);
モデル
<?php class Viewer extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer'; } public function relations() { return array( 'watched_movies' => array(self::HAS_MANY, 'ViewerWatchedMovie', 'viewer_id'), ); } }
<?php class Movie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'movie'; } }
<?php class ViewerWatchedMovie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer_watched_movie'; } public function relations() { return array( 'movie' => array(self::BELONGS_TO, 'Movie', 'movie_id'), ); } }
レイジーローディング
<?php $viewer = Viewer::model()->findByPk(1); foreach ($viewer->watched_movies as $watched) { echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ') . $watched->movie->title; }
イーガーローディング
<?php $viewers = Viewer::model()->with(array( 'watched_movies'=>array( 'condition'=>'watched_movies.liked=:liked', 'params'=>array(':liked'=>true), ), ))->findAll(); foreach ($viewers as $viewer) { foreach ($viewer->watched_movies as $watched) { echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ') . $watched->movie->title; } }
まとめのような
CActiveRecordの MANY_MANY, HAS_MANY...は単なるクラス名なので、次のようにオレオレリレーションのクラス名「OreOreRelation」でもいいんです。
<?php class Viewer extends CActiveRecord { .... public function relations() { return array( 'movies' => array('OreOreRelation', 'Movie', 'viewer_watched_movie(viewer_id, movie_id)', 'pivot_model'=>'ViewerWatchedMovie', ), ); } }
で、オレオレリレーションでSQL書き換えればいいかと思ったらさにあらず、SELECT..JOIN...生成はリレーションクラスでなく、CActiveFinderというかCJoinElementがやっています。
中間テーブルを取得するリレーションを作ってみたのですが、CJoinElementの 「if (relation instanseof CManyManyRelation) 」している辺りを探りながらパッチ当てることで、レイジーローディングだけどうにかできました。書くまでもなく茨の道です。
そんなわけで、いまのところエレガントな解決方法はHasMany, BelongsToだな、というところに落ち着きました。まえに弱点と書きましたが、今回のような設計で使ってくれと言うことなんだと思います。