YiiでMANY_MANYの関係を扱う

イベントとイベントの属するカテゴリがあるとします。各イベントは複数のカテゴリに所属する。またカテゴリには多くのイベントが登録されているような関係を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,
  ));
}
投稿日:
カテゴリー: php タグ: