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 にアクセスします。こんなページが表示されます。
「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のインストールは済ませてください。
- データベース接続の確立を設定する
- テーブルをマイグレーションで作成する
- Giiでテーブルからモデルを生成する
- モデルへリレーションを設定する
- フィクスチャでテストデータを定義する
- ユニットテストでリレーションを検証する
作成するテーブル、リレーションはリレーショナルアクティブレコード - ガイドを参照してください。
データベース接続の確立
データベース接続はデータベース接続の確立を参照しつつ設定します。作成するテーブルには接頭子「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に対応する中間テーブルのカラム名 ); } }
データベースマイグレーションを使ってみる(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を駆使する
とかで解決できないか悩んでいます。モッククラスを書きたくないし、何よりサービスレイヤのテストでフィクスチャに依存したくないんです。