Yiiでも中間テーブルにアクセスしたい
またこのネタです。
YiiFrameworkのCManyManyRelationは中間テーブルへのアクセスをサポートしていません。ならばオレオレリレーションによってそれを可能にしてみます。前エントリのまとめと反するのですが、まあ、あそびばってことで。
実現のためにCJoinElementを改造します。まだまだ中途半端な状態ですが載せておきます。気がむいたら更新するかもしれません。
2012.10.25 実験段階 とりあえずレイジーローディング
リレーション
まずオレオレリレーションモデルです。
protected/components/ManyManyPivotRelation.php
<?php class ManyManyPivotRelation extends CManyManyRelation { public $pivotModel = null; public $pivotWith = array(); }
モデル
続いてモデル
<?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( 'movies_with_pivot' => array("ManyManyPivotRelation", 'Movie', 'viewer_watched_movie(viewer_id, movie_id)', 'pivotModel' => 'ViewerWatchedMovie', // <- 中間テーブルモデル 'pivotWith' => array('id','viewer_id', 'movie_id', 'liked',), // <-中間デーブルのカラム ), ); } }
viewer_watched_movie テーブルのPKはIDです。
<?php class ViewerWatchedMovie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer_watched_movie'; } }
<?php class Movie extends CActiveRecord { public $pivot; // <- ここに中間テーブルのモデルが入る public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'movie'; } }
CJoinElement
次はCJoinElementに手を入れます。hack begin - hack end を挿入してください。
リレーションがManyManyPivotRelationならば、selectするカラムを追加し、select結果からpivotインスタンスを生成し、子モデルの $record->pivot にセットしています。
YiiFramework/framework/db/ar/CActiveFinder.php
<?php class CJoinElement { private function applyLazyCondition($query,$record) { $schema=$this->_builder->getSchema(); $parent=$this->_parent; if($this->relation instanceof CManyManyRelation) { .... if($parentCondition!==array() && $childCondition!==array()) { $join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON '; $join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')'; if(!empty($this->relation->on)) $join.=' AND ('.$this->relation->on.')'; $query->joins[]=$join; foreach($params as $name=>$value) $query->params[$name]=$value; } else throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name))); // hack begin ---------- if($this->relation instanceof ManyManyPivotRelation) { // add pivot columns // TODO table.column -> t0_c0 foreach ($this->relation->pivotWith as $col) { $query->selects[]=$joinAlias.'.'.$schema->quoteColumnName($col); } } // hack end ---------- } else { .... } } private function populateRecord($query,$row) { .... if(isset($this->records[$pk])) $record=$this->records[$pk]; else { $attributes=array(); $aliases=array_flip($this->_columnAliases); foreach($row as $alias=>$value) { if(isset($aliases[$alias])) $attributes[$aliases[$alias]]=$value; } $record=$this->model->populateRecord($attributes,false); foreach($this->children as $child) { if(!empty($child->relation->select)) { $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation); } } $this->records[$pk]=$record; // hack begin ---------- if($this->relation instanceof ManyManyPivotRelation) { // make pivot instance // TODO t0_c0 -> column_name $p = new $this->relation->pivotModel; $record->pivot = $p->populateRecord($row,false); } // hack end ---------- } .... } }
ビュー
<?php $viewers = Viewer::model()->findAll(); foreach($viewers as $viewer) { foreach ($viewer->movies_with_pivot as $movie) { echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ') . $movie->title; } }
結果
hoge didn’t like foo hoge liked bar piyo liked foo piyo didn’t like bar