イベントとイベントの属するカテゴリがあるとします。各イベントは複数のカテゴリに所属する。またカテゴリには多くのイベントが登録されているような関係をMANY_MANYの関係といいます。
このような関係を扱う場合、テーブルの設定、モデルでの設定、コントローラでの利用方法をまとめておきます。
テーブルの設定
イベントデータを格納するtbl_eventテーブル、イベントのカテゴリデータを格納するtbl_categoryとイベントがどのカテゴリに所属するかを格納するtbl_event_categoryテーブルを以下のように作成します。作成にはYiiのマイグレーションを利用します。
class m121120_075108_create_event_table extends CDbMigration { protected $options = 'ENGINE=InnoDB DEFAULT CHARSET=utf8'; public function up() { //tbl_eventテーブル $this->createTable('tbl_event',array( 'id'=>'pk', 'title'=>'string not null', 'detail'=>'text not null', 'created'=>'datetime', 'modified'=>'datetime' ), $this->options); //tbl_categoryテーブル $this->createTable('tbl_category',array( 'id'=>'pk', 'name'=>'VARCHAR(128) not null', 'position'=>'integer not null', ), $this->options); //tbl_event_categoryテーブル $this->createTable('tbl_event_category',array( 'event_id'=>"integer NOT NULL", 'category_id'=>"integer not null", "PRIMARY KEY (event_id,category_id)", ), $this->options); //外部キーの設定 $this->addForeignKey('FK_event', 'tbl_event_category', 'event_id', 'tbl_event', 'id', 'CASCADE', 'CASCADE'); $this->addForeignKey('FK_category', 'tbl_event_category', 'category_id', 'tbl_category', 'id', 'CASCADE', 'CASCADE'); } public function down() { $this->dropTable('tbl_event'); $this->dropTable('tbl_category'); $this->dropTable('tbl_event_category'); echo "m121120_075108_create_event_table does not support migration down.\n"; return false; } }
モデルの設定
giiを利用して、Eventモデル、Categoryモデル、EventCategoryモデルの3つを作成します。
作成後、Eventモデルの
public function relations() { return array( 'tblCategories' => array(self::MANY_MANY, 'Category', 'tbl_event_category(event_id, category_id)'), ); }
は
public function relations() { return array( 'categories' => array(self::MANY_MANY, 'Category', 'tbl_event_category(event_id, category_id)'), ); }
のように修正しておきます。これで、EventモデルとCategoryモデルがMANY_MANYの関係であることの設定ができました。
イベントを登録、更新する
イベントデータ登録後、イベントの属するカテゴリのデータも更新できるようにEventモデルのafterSave関数を利用して、tbl_event_categoryテーブルを更新するようにします。
protected function afterSave() { parent::afterSave(); EventCategory::model()->updateCategory($model->id,$model->category1,$model->category2); }
EventCategoryモデルには、以下の処理を追加しておきます。
public function updateCategory($event_id, $category1,$cateogory2) { //すでに登録されている場合はすべて削除する。 $this->deleteCategory($event_id); $cat1=is_array($category1) ? $category1 : explode(',',$category1); $cat2=is_array($cateogory2) ? $cateogory2 : explode(',',$cateogory2); $arr=array(); if(!empty($cat1)) $arr=array_merge($arr,$cat1); if(!empty($cat2)) $arr=array_merge($arr,$cat2); $arr=array_unique($arr); //新規に登録する。 foreach($arr as $category) { if(empty($category)) continue; $this->isNewRecord = true; $this->primaryKey = NULL; $this->event_id=$event_id; $this->category_id=$category; $this->save(false); } } public function deleteCategory($event_id) { $this->deleteAll('event_id=:event_id',array(':event_id'=>$event_id)); }
EventコントローラのactionCreateやactionUpdateの
if($model->save()) $this->redirect(array('view','id'=>$model->id));
の部分については
$transaction = $model->dbConnection->beginTransaction(); // transaction try { $model->save(); $transaction->commit(); // commit $this->redirect(array('view','id'=>$model->id)); } catch(Exception $e) { $transaction->rollBack(); // rollBack Yii::app()->user->setFlash('error','更新に失敗しました。'); }
のようにトランザクションを利用するようにします。
イベントを削除する
イベントデータ削除後については、テーブルの作成時に外部キーで ON DELETE CASCADE を設定しているので、tbl_eventテーブルのデータを削除するとtbl_event_categoryのデータも連動して削除されるので、PHPで削除処理を書く必要はありません。
もし、DBのほうで外部キーをサポートしていないなら、EventモデルのafterDelete関数を利用してtbl_event_categoryテーブルのデータも削除するようにしたらよいでしょう。
protected function afterDelete() { parent::afterDelete(); EventCategory::model()->deleteCategory($this->getPrimaryKey()); }
EventコントローラのactionDeleteも
$this->loadModel($id)->delete();
の部分を
$model=$this->loadModel($id); $transaction = $model->dbConnection->beginTransaction(); // transaction try { $model->delete(); $transaction->commit(); // commit } catch(Exception $e) { $transaction->rollBack(); // rollBack }
のように修正します。
イベント名やカテゴリで検索する
検索ページで入力したイベント名やカテゴリなどの検索条件をもとに、データを検索し、検索結果ページに表示するような処理を組み込みます。
Eventコントローラでは、以下のようにsearchアクションを作成します。
public function actionSearch() { $model=new Event(); if(isset($_GET['Event'])) { $model->attributes=$_GET['Event']; if ($model->validate()) { $dataProvider = $model->loadAll(); $this->render('result', array('dataProvider'=>$dataProvider)); } else $this->render('search',array('model'=>$model)); } else $this->render('search',array('model'=>$model)); }
検索ページとしてevent/search.phpビューを作成します。
<?php $form=$this->beginWidget('bootstrap.widgets.TbActiveForm',array( 'id'=>'search-form', )); ?> <?php echo $form->dropDownListRow($model,'category',Category::categories(),array('class'=>'span5','empty'=>'--')); ?> <?php echo $form->textFieldRow($model,'title',array('class'=>'span5','maxlength'=>128)); ?> <div class="form-actions"> <?php $this->widget('bootstrap.widgets.TbButton', array( 'buttonType'=>'submit', 'type'=>'primary', 'label'=>'Search', )); ?> </div> <?php $this->endWidget(); ?>
なお、Category::categories()はselectボックスの選択肢としてカテゴリのリストを取り出す関数です。Cateogoryモデル内で以下のようになっています。
private static $_categories=array(); public static function categories() { if(!isset(self::$_categories) || empty(self::$_categories)) self::loadCategories(); return self::$_categories; } private static function loadCategories() { self::$_categories=array(); $models=self::model()->findAll(array( 'select'=>'id,name', 'order'=>'position', )); foreach($models as $model) self::$_categories[$model->id]=$model->name; }
また、tbl_eventフィールドにないcategory属性を利用するため、Eventモデル内には、
public $category=null;
を追加しておく必要があります。この他にもrules関数やattributeLabels関数にも適切に設定しなければなりません。
次に、検索結果ページとしてevent/result.phpビューを用意します。
<?php $this->widget('bootstrap.widgets.TbListView', array( 'dataProvider' => $dataProvider, 'itemView' => '_result', )); ?>
event/_result.phpは以下のとおりです。イベントが属するカテゴリをforeachで書き出しています。
<div class="view row"> <?php echo CHtml::encode($data->title); ?><br /> <?php foreach($data->cateogories as $category): ?> <?php echo CHtml::encode($category->name); ?>, <?php endforeach; ?> <?php echo CHtml::encode($data->detail); ?> </div>
最後に、dataProviderを用意するloadAll関数ですが、Eventモデル内で以下のようになっています。
public function loadAll() { $criteria = new CDbCriteria; if($this->title!='') { $criteria2 = new CDbCriteria; $criteria2->compare('title',$this->title,true); $criteria->mergeWith($criteria2); } if($this->category) { $criteria->mergeWith(array( 'with' => array( 'categories'=>array( 'condition'=>'categories.id=:id', 'params'=>array(':id'=>$this->category), ), ), 'group'=>'t.id', 'together'=>true)); } return new CActiveDataProvider(get_class($this), array( 'criteria' => $criteria, )); }