tjtjtjのメモ

自分のためのメモです

yiiカイドを横断してみる リレーション編(2/3) - モデル作成

今回はモデルを作ります。

Gii準備

yiic shell のコード生成機能は、バージョン 1.1.2 以降、非推奨(depricated)となっています。

http://www.yiiframework.com/doc/guide/1.1/ja/quickstart.first-app-yiic

とのことなのでyiicを使わずGiiを使ってモデルを生成します。Giiを使うにはmain.phpへ設定が必要です。Giiを使うを参照しつつ設定します。

/webapp/protrected/config/main.php

<?php
return array(
	'modules'=>array(
		'gii'=>array(
			'class'=>'system.gii.GiiModule',
			'password'=>'pick up a password here', <<適当に変更
 		),
	),
);

Giiでモデル生成する

URL http://hostname/path/to/index.php?r=gii にアクセスします。こんなページが表示されます。
f:id:tjtjtjofthedead:20110807130702j:image:medium:right

「Table Name」にはtablePrefixを省略しないテーブル名「tbl_post」、「Model Class」には「Post」を入力しpreviewをクリックするとモデル実装のプレビューが表示されます。続いてgenerateをクリックするとPost.phpが生成されます。「Build Rerations」をチェックしておけば、外部キーに基づきリレーションも設定してくれます。

/webapp/protected/models/Post.php (の一部)

<?php
class Post extends CActiveRecord
{
	/**
	 * Returns the static model of the specified AR class.
	 * @return Post the static model class
	 */
	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}

	/**
	 * @return string the associated database table name
	 */
	public function tableName()
	{
		return '{{post}}';
	}

	/**
	 * @return array relational rules.
	 */
	public function relations()
	{
		// NOTE: you may need to adjust the relation name and the related
		// class name for the relations automatically generated below.
		return array(
			'author' => array(self::BELONGS_TO, 'User', 'author_id'),
			'postCategories' => array(self::HAS_MANY, 'PostCategory', 'post_id'), //<-PostCategoryモデルを参照している
		);
	}

}

ここでもテーブル名が{{}}でブレースされていますね。その調子で計5モデルを生成します。

  • tbl_post -> Post
  • tbl_category -> Category8
  • tbl_user -> User
  • tbl_profile -> Profile
  • tbl_post_category -> PostCategory

PostモデルからpostCategoriesリーレーションでPostCategorへリレーションしていますが、本来のリレーション先はCaregoryです。これは後で修正します。

リレーションを設定する

リレーショナルアクティブレコード - ガイドのようにリレーションを定義します。大体はGiiで生成したままでいいのですが、MANY_MANYは手で修正する必要があるようです。
PostからCategoryを MANY_MANY でリレーションするよう修正します。リレーションにあわせてクラスコメントも修正します。

/webapp/protected/models/Post.php (の一部)

<?php
/**
 * This is the model class for table "{{post}}".
 *
 * The followings are the available columns in table '{{post}}':
 * @property integer $id
 * @property string $title
 * @property string $content
 * @property string $create_time
 * @property integer $author_id
 *
 * The followings are the available model relations:
 * @property User $author
 * @property Categories[] $categories  << ここ修正
 */
class Post extends CActiveRecord
{
	public function relations()
	{
		return array(
			'author' => array(self::BELONGS_TO, 'User', 'author_id'),
			'categories'=>array(self::MANY_MANY, 'Category',
				'{{post_category}}(post_id, category_id)'),  //{{}}でtablePrefixを適用する
			//categories'=>array(self::MANY_MANY, 'Category', // ガイドの例
			//    'tbl_post_category(post_id, category_id)'),
			//'postCategories' => array(self::HAS_MANY, 'PostCategory', 'post_id'), //Gii で生成
		);
	}
}

/webapp/protected/models/User.php (の一部)

<?php
/**
 * This is the model class for table "{{user}}".
 *
 * The followings are the available columns in table '{{user}}':
 * @property integer $id
 * @property string $username
 * @property string $password
 * @property string $email
 *
 * The followings are the available model relations:
 * @property Post[] $posts
 * @property Profile $profile
 */
class User extends CActiveRecord
{
	public function relations()
	{
		return array(
			'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
			'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
		);
	}
}

yiiカイドを横断してみる リレーション編(1/3) - テーブル作成

初見の人がリレーショナルアクティブレコード - ガイドをみてテーブルを作成しモデルを作成しリレーション定義しそれを検証できるんだろうか、ガイド記事は断片的過ぎやしないか、横断的なまとまった情報ほしいなあ、と思い書いてみました。。
yiiのカイドページを参照しつつ、リレーション作成とその検証まで辿ります。DBエンジンにはMySQL.Innodbを使います。

アジェンダ

yii のガイドを辿ってデータベース接続からリレーションの検証までを辿ります。Yiiアプリケーションの生成、データベースの生成(CREATE DATABASE)、PHPUnitのインストールは済ませてください。

作成するテーブル、リレーションはリレーショナルアクティブレコード - ガイドを参照してください。

データベース接続の確立

データベース接続はデータベース接続の確立を参照しつつ設定します。作成するテーブルには接頭子「tbl_」が付いています。これを main.php, console.php に設定します。console.phpで定義した設定は、テーブル生成時使われます。

/webapp/protrected/config/main.php

<?php
return array(
	'components'=>array(
		'db'=>array(
			'connectionString' => 'mysql:host=localhost;dbname=test',
			'emulatePrepare' => true,
			'username' => 'root',
			'password' => '',
			'charset' => 'utf8',
			'tablePrefix' => 'tbl_', // <<< ここに接頭子を定義する
		),
	),
);

/webapp/protrected/config/console.php

<?php
return array(
	'components'=>array(
		'db'=>array(
			'connectionString' => 'mysql:host=localhost;dbname=test',
			'emulatePrepare' => true,
			'username' => 'root',
			'password' => '',
			'charset' => 'utf8',
			'tablePrefix' => 'tbl_', // <<< ここに接頭子を定義する
		),
	),
);

マイグレーション作成

マイグレーションを作成します。マイグレーションは「yiic migrate create <マイグレーション名>」で生成します。今回はファーストリリースということでマイグレーション名「first_release」としました。

> cd webapp\protected
> yiic migrete create first_release

次に生成したマイグレーションにテーブル生成スクリプトを書きます。スキーマを操作するクエリをビルドする - クエリビルダ - ガイドを参照してください。
テーブル名は「tbl_post」としてもいいですが、{{}}でブレースすると接頭子「tbl_」を付与してくれるので利用します。
エンジンを念のためINNODBにしています。

/webapp/protrected/migrate/m110720_122652_first_release.php

<?php
class m110720_122652_first_release extends CDbMigration
{
	public function up()
	{
 		$this->createTable('{{post}}', array(
			'id' => 'pk',
			'title' => 'string NOT NULL',
			'content' => 'text',
			'create_time' => 'datetime',
			'author_id' => 'integer',
 		));
 		$this->execute('ALTER TABLE {{post}} ENGINE = INNODB;'); 
 		$this->createTable('{{post_category}}', array(
			'post_id' => 'integer',
 			'category_id' => 'integer',
 		));
 		$this->execute('ALTER TABLE {{post_category}} ENGINE = INNODB;'); 
 		$this->createTable('{{category}}', array(
			'id' => 'pk',
 			'name' => 'string NOT NULL',
 		));
 		$this->execute('ALTER TABLE {{category}} ENGINE = INNODB;'); 
 		$this->createTable('{{user}}', array(
			'id' => 'pk',
 			'username' => 'string NOT NULL',
 			'password' => 'string',
 			'email' => 'string',
 		));
 		$this->execute('ALTER TABLE {{user}} ENGINE = INNODB;'); 
 		$this->createTable('{{profile}}', array(
			'owner_id' => 'pk',
 			'photo' => 'binary',
 			'website' => 'string',
 		));
 		$this->execute('ALTER TABLE {{profile}} ENGINE = INNODB;'); 

 		$this->addForeignKey('fk_post_owner_id', '{{post}}',
 			'author_id', '{{user}}', 'id', 'CASCADE', 'CASCADE');
 		$this->addForeignKey('fk_profile_owner_id', '{{profile}}',
 			'owner_id', '{{user}}', 'id', 'CASCADE', 'CASCADE');
 		$this->addForeignKey('fk_post_category_1', '{{post_category}}',
 			'post_id', '{{post}}', 'id', 'CASCADE', 'CASCADE');
 		$this->addForeignKey('fk_post_category_2', '{{post_category}}',
 			'category_id', '{{category}}', 'id', 'CASCADE', 'CASCADE');
	}

	public function down()
	{
 		$this->dropForeignKey('fk_post_owner_id', '{{post}}');
 		$this->dropForeignKey('fk_profile_owner_id', '{{profile}}');
 		$this->dropForeignKey('fk_post_category_1', '{{post_category}}');
 		$this->dropForeignKey('fk_post_category_2', '{{post_category}}');

		$this->dropTable('{{post}}');
		$this->dropTable('{{post_category}}');
		$this->dropTable('{{category}}');
		$this->dropTable('{{user}}');
		$this->dropTable('{{profile}}');
	}
}

テーブル生成 - マイグレーションからテーブルを生成する

「yiic migrate」でマイグレーションを適用し、テーブルを生成します。

> cd \webapp\protected\
> yiic migrate

Yii Migration Tool v1.0 (based on Yii v1.1.8)

Total 1 new migration to be applied:
    m110720_122652_first

Apply the above migration? [yes|no] y
*** applying m110720_122652_first
    > create table {{post}} ... done (time: 0.096s)
    > create table {{post_category}} ... done (time: 0.068s)
    > create table {{category}} ... done (time: 0.111s)
    > create table {{user}} ... done (time: 0.151s)
    > create table {{profile}} ... done (time: 0.077s)
    > add foreign key fk_post_owner_id: {{post}} (author_id) references {{user}}
 (id) ... done (time: 0.152s)
    > add foreign key fk_profile_owner_id: {{profile}} (owner_id) references {{u
ser}} (id) ... done (time: 0.178s)
    > add foreign key fk_post_category_1: {{post_category}} (post_id) references
 {{post}} (id) ... done (time: 0.152s)
    > add foreign key fk_post_category_2: {{post_category}} (category_id) refere
nces {{category}} (id) ... done (time: 0.143s)
*** applied m110720_122652_first (time: 0.002s)


Migrated up successfully.

作成したテーブル、外部キーをphpmyadminとかで確認してください。

リレーショナルアクティブレコードを使ってみる

yii1.1.8 リリースとともにガイドのリレーショナルアクティブレコードページが更新され、英語ガイドページとの同期が取れました。これまで、日本語ガイドと英語ガイドを行き来して面倒だったんです。翻訳の皆さんありがとうございます。

リレーション宣言で特にわからないのが「ForeignKey」の書き方でした。以下は試行錯誤してやっと解決したときのメモです*1

  'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...付加オプション)

HAS_ONE

ガイド例「User」「Profile」の関係です。次はガイドのコードにコメントを付与したものです。

<?php
class User extends CActiveRecord
{
    public function relations()
    {
        return array(

            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
            //VarName        RelationType    ClassName  ForeignKey

            //VarName:    Userクラスに拡張するフィールド名
            //ClassName:  拡張するフィールドに対応するCActiveRecordクラス
            //ForeignKey: こちら(tbl_user)のPKに対応する相手カラムを指定する 
            //            つまり tbl_profile.owner_id であり Profile->owner_id
        );
    }
}

HAS_MANY

ガイド例「User」「Post」の関係です。

<?php
class User extends CActiveRecord
{
    public function relations()
    {
        return array(

            'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
            //VarName      RelationType    ClassName  ForeignKey

            //VarName:    Userクラスに拡張するフィールド名
            //ClassName:  拡張するフィールドに対応するCActiveRecordクラス
            //ForeignKey: こちら(tbl_user)のPKに対応する相手のカラムを指定する
            //            つまり tbl_post.author_id であり Post->author_id
        );
    }
}

BELONGS_TO

ガイド例「Post」「User」の関係です。

<?php
class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
            //VarName       RelationType      ClassName  ForeignKey

            //VarName:    Postクラスに拡張するフィールド名
            //ClassName:  Postクラスに拡張するフィールドに対応するCActiveRecordクラス
            //ForeignKey: 相手(tbl_user)のPKに対応するこちらのカラム名を指定する
            //            つまり tbl_post.author_id であり Post->author_id
        );
    }
}

MANY_MANY

ガイド例「Post」「Category」と中間のテーブル「tbl_post_category」の関係です。

<?php
class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'categories'=>array(self::MANY_MANY, 'Category', 'tbl_post_category(post_id, category_id)'),
            //VarName           RelationType      ClassName   ForeignKey

            //VarName:    Postクラスに拡張するフィールド名
            //ClassName:  拡張するフィールドに対応するCActiveRecordクラス
            //ForeignKey: 中間テーブル名(カラムこちら, カラムあっち)
            //            - tablePrefixを効かせるなら{{}}でかこむ
            //            - カラムこちら  :こちら(tbl_post)のPKに対応する中間テーブルのカラム名
            //            - カラムあっち  :あっち(tbl_category)のPKに対応する中間テーブルのカラム名
        );
    }
}

*1:更新前の日本語ガイドはER図とコードにズレがありました。さらにMyISAMを使うときは外部キーが使えないから、外部キーを模したコメントを付与する必要があるとかなんとか...で試行錯誤を繰り返しました。2ヶ月以上前のことですが

データベースマイグレーションを使ってみる(2)

昨日の続き

テーブルプレフィックス

console.php で tablePrefix を指定したとき、マイグレーションのテーブル名を「{{」「}}」で囲むと、プレフィックスつきでCREATEしてくれました。

<?php
// console.php
return array(
  'components'=>array(
    :
    'db'=>array(
      :
      'tablePrefix' => 'test_',
    ),
  ),
);
<?php
class m110720_122652_first extends CDbMigration
{
  public function up()
  {
    $this->createTable('{{tbl_news}}', array(
      'id' => 'pk',
      'title' => 'string NOT NULL',
      'content' => 'text',
    ));
  }
}

↓↓↓↓↓

CREATE TABLE test_tbl_news (....)

テーブルプレフィックス(2)

$this->execute() でもプレフィックスつきでCREATEしてくれます。

<?php
class m110720_122652_first extends CDbMigration
{
  public function up()
  {
    $this->execute(<<<SQL
CREATE TABLE `{{tbl_news}}` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM;
SQL
    );
  }

↓↓↓↓↓

CREATE TABLE test_tbl_news (....)

データベースマイグレーションを使ってみる

使い方はガイドをみれば大体わかるはず。なので、ちょっと引っかかったことを残します。

接続先はconsole.phpのDB

yiic migrate は webapp/protected/config/console.php の DB を使う。yiic model は main.php の DB を使ったので油断してました。

カラム型の変換

<?php
class m110720_122652_first extends CDbMigration
{
  public function up()
  {
    $this->createTable('tbl_news', array(
      'id' => 'pk',
      'title' => 'string NOT NULL',
      'content' => 'text',
    ));
  }
}

とガイドのように書くとMYSQLの場合、次のようなCREATE TABLEになるようです。

  CREATE TABLE `tbl_news` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `title` varchar(255) NOT NULL,
    `content` text,
    PRIMARY KEY (`id`)
  ) ENGINE=InnoDB;
  • 'pk' → int(11) NOT NULL AUTO_INCREMENT
  • 'string NOT NULL' → varchar(255) NOT NULL
  • 'text' → text

になってます。DBMSの型表現の違いを吸収するためなんでしょうか。
$this->createTable()のカラム指定とSQLのカラム型変換は英語ガイドコメントにあります。というかここか。

これは書けない
<?php
  $this->createTable('tbl_news', 
    array(
      'id' => 'int(5) NOT NULL AUTO_INCREMENT',
      'title' => 'string NOT NULL',
      'content' => 'text',
    ),
    'ENGINE=MyISAM'
  );

とすると「AUTO_INCREMENT」「ENGINE=MyISAM」が通りませんでした。うーん。

SQLで書ける

勝手にInnoDBにされてもなあ、INT(11)に UNSIGNED 付けたいなあ、PlayFrameworkみたいに生SQLマイグレーションできないのと思ったら「$this->execute()」でSQLをそのまま実行できました。

<?php
class m110720_122652_first extends CDbMigration
{
  public function up()
  {
    $this->execute(<<<SQL
CREATE TABLE `tbl_news` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `content` text,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM;
SQL
    );
  }

これでいいんじゃない。

ファイルアップロード

yiiでファイルアップロードしてみる。アップロードしたファイルはMySQL BLOBに保存する。

テーブルを作る

create table `file` (
  `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(255) not null,
  `type` varchar(64) not null,
  `size` float not null,
  `content` BLOB not null
)
;

モデルを作る

yiifile\protected\models\File.php

<?php
class File extends CActiveRecord
{
	public $name;
 	public $type;
 	public $size;
 	public $content;

 	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}
 	
}

createアクションを作る

yiifile\protected\controllers\FilesController.php

<?php
class FilesController extends Controller
{
	public function actionCreate()
	{
		$model = new File;

		if(isset($_POST['File']))
		{
			$temp = CUploadedFile::getInstance($model, 'file');
			$model->name = $temp->name;
			$model->type = $temp->type;
			$model->size = $temp->size;
			$model->content = file_get_contents($temp->tempName);
			unlink($temp->tempName);

			if($model->save())
			$this->redirect(array('view','id'=>$model->id));
		}

		$this->render('create',array(
			'model'=>$model,
		));
	}
}

createビューを作る

yiifile\protected\views\files\create.php

<h1>Create File</h1>

<?php $form=$this->beginWidget('CActiveForm', array(
	'id'=>'entry-form',
	'htmlOptions' => array('enctype'=>'multipart/form-data'),
)); ?>

	<div class="row">
		<?php echo $form->fileField($model,'file'); ?>
	</div>

	<div class="row buttons">
		<?php echo CHtml::submitButton('Save'); ?>
	</div>

<?php $this->endWidget(); ?>

Createページでファイルを選択し、SaveボタンをクリックするとFileテーブルに入ってるはず。

viewアクションを作る

アップロードしたファイルをブラウザで表示してみよう。

yiifile\protected\controllers\FilesController.php

<?php
class FilesController extends Controller
{
	public function actionView()
	{
		$model = File::model()->findByPk($_GET['id']);

		header('Content-Type: '.$model->type);
		echo $model->content;
	}
}

これでアップロードするとブラウザで表示する。

モデルのモックを使ってテストしたいのだが

うーん。

yiiへの期待のひとつに、PHPUnitが使えてPHPUnitのモックを利用できることがあったのですが、厳しい感じです。

id:hiromi2424:20110306 にあるような、サービスレイヤを設けたいのです。がががががががががが、Yiiは、CakePHPのClassRegistryのようなものを介さず、直接newしたりモデルのstaticメソッドににアクセスしています。これを解決するmockクラスを簡単に生成する方法を見出せずにいます。

  • モデルクラスへアクセスする前に、モデルのモッククラスをinculdeする
  • PHPUnitのgetMocK、getMockClassを駆使する

とかで解決できないか悩んでいます。モッククラスを書きたくないし、何よりサービスレイヤのテストでフィクスチャに依存したくないんです。