Jobeet - 3日目: データモデル
Jobeet - Day 3: The Data Model - Symfony
ようやく少しPHPコードが出てきました。まだ1.2になって新しいなー、と感じるところはありません。Outputzが凄い勢いで枚数カウントされててちょっぴりうれしかったりします。
あと、コメント欄でFabienがDoctrine版のJobeetも提供するつもりだよ、みたいなことを言ってるのが非常に気になります。
前回までのJobeet
みんなテキストエディタを開きたくてうずうずしていて、今日のチュートリアルで覚えたことをPHPで書き留め、開発手法を身につけるでしょう。我々はORMを使ってデータベースと情報をやり取りするデータモデルを定義します。そして、アプリケーションの最初のモジュールを構築します。symfonyはみんなのためにたくさんの仕事をこなしてくれますが、みなさんはたくさんのPHPコードを書くことなく十分に機能的なWebモジュールを使えるようになります。
リレーションモデル
昨日説明したユーザストーリーではプロジェクトの主なオブジェクトについて説明しています。それはjobs, affiliates, categoriesです。下図は対応するエンティティ関係図です。
ストーリーで説明したカラムに加えて、いくつかのテーブルには created_at フィールドも追加しています。symfonyはそのフィールドを見分けて、レコードが生成されたとき現在のシステム時刻を値としてセットします。updated_at フィールドも同様なものです。レコードが更新されるとシステム時刻をセットします。
スキーマ
jobs, affiliates, categoriesを保存するために、当然リレーショナルデータベースが必要となります。
しかしsymfonyのようなオブジェクト指向のフレームワークではいつでもオブジェクトを操作したいと考えます。例えば、データベースからレコードを取得するSQL文を書く代わりにオブジェクトを使う方を好みます。
リレーショナルデータベースの情報はオブジェクトモデルとしてマッピングされなければなりません。これはありがたいことにORMツールを使って実現でき、symfonyでは2つのORM(PropelとDoctrine)をバンドルしています。このチュートリアルではPropelを使っていきます。
ORMはテーブルと生成する関連クラスとの関係を説明が必要です。スキーマの記述には2つの方法があります。それは既存のデータベースから作る方法と、手書きで作る方法の2つです。
グラフからデータベースを構築したり(fabFORCE.net)、schema.xmlを直接生成する()ツールもあります。
データベースはまだ存在しておらず、Jobeetのデータベースは不可視でありたいと考えています。さぁ、空の config/schema.yml ファイルを編集してschemaファイルを作ってみましょう!
# config/schema.yml propel: jobeet_category: id: ~ name: { type: varchar(255), required: true } jobeet_job: id: ~ category_id: { type: integer, foreignTable: jobeet_category, foreignReference: id, required: true } type: { type: varchar(255) } company: { type: varchar(255), required: true } logo: { type: varchar(255) } url: { type: varchar(255) } position: { type: varchar(255), required: true } location: { type: varchar(255), required: true } description: { type: longvarchar, required: true } how_to_apply: { type: longvarchar, required: true } token: { type: varchar(255), required: true, index: unique } is_public: { type: boolean, required: true, default: 1 } is_activated: { type: boolean, required: true, default: 0 } email: { type: varchar(255), required: true } expires_at: { type: timestamp, required: true } created_at: ~ updated_at: ~ jobeet_affiliate: id: ~ url: { type: varchar(255), required: true } email: { type: varchar(255), required: true, index: unique } token: { type: varchar(255), required: true } is_active: { type: boolean, required: true, default: 0 } created_at: ~ jobeet_job_affiliate: job_id: { type: integer, foreignTable: jobeet_job, foreignReference: id, required: true, primaryKey: true, onDelete: cascade } affiliate_id: { type: integer, foreignTable: jobeet_affiliate, foreignReference: id, required: true, primaryKey: true, onDelete: cascade }
もしSQL文でテーブル作ることにしているのなら、propel:build-schema タスクを実行して一致する schema.ymlファイルを生成してください。
スキーマはYAMLフォーマット内でエンティティ関係図の構成に直接変換されます。
schema.yml ファイルは全てのテーブルとカラムの説明を含んでいます。各カラムは下記の情報をつけて記述されます。
- type
- カラムの種類(boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, blob, clob)
- required
- trueをセットするとNOT NULLが設定されます
- index
- trueをセットするとINDEXが張られて、uniqueをセットするとUNIQUE INDEXが張られます
~ がセットされたカラム(id, craeted_at, updated_at)は、symfonyが適切な設定を推測して設定してくれます。(idはPRIMARY KEYと推測し、 created_atやupdated_atはTIMESTAMPと推測)
ondelete属性は外部キーのON DELETEの動作を定義し、PropelではCASCADE、SETNULL、RESTRICTをサポートします。例えばjobレコードが削除されると、jobeet_job_affiliateテーブルの関連する全てのレコードがデータベースによって削除されます。もしデータベースがこの機能に対応していなければPropelによって削除されます。
データベース
symfonyはPDOが対応するデータベース全てをサポートしています(MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...)。PDOはPHPにバンドルされてるデータベース抽象レイヤです。
このチュートリアルではMySQLを使いましょう。
$ mysqladmin5 -uroot create jobeet $ mysql5 -uroot GRANT ALL ON jobeet.* TO sfuser@localhost IDENTIFIED BY "sfpass";
symfonyにデータベースの設定を教えます。
$ symfony configure:database "mysql:host=localhost;dbname=jobeet" sfuser sfpass
configure:database タスクは3つの引数を持ちます。それはPDO DSNとDBユーザ名とDBパスワードです。もし開発サーバなどでパスワードを設定していないのなら3つ目に引数は省略可能です。
configure:database タスクは config/database.yml ファイルに結果を保存します。タスクを使う代わりに手動で編集してもOKです。
ORM
schema.ymlに書いたデータベースの説明のおかげで、PDOのビルトイン関数を利用してテーブルを生成するためのSQL文を作れます。
$ symfony propel:build-sql
propel:build-sql タスクはdata/sqlディレクトリに設定されているデータベースエンジンに最適化されたSQL文を生成します。
実際にデータベース上にテーブルを生成するには、propel:insert-sql タスクを実行する必要があります。
$ symfony propel:insert-sql
このタスクはテーブルを再生成する前に現在のテーブルを削除するので、確認ダイアログが出てきます。--no-confirmationオプションを使えば、その確認ダイアログを無視できるのでバッチに組みこむときは便利です。
$ symfony propel:insert-sql --no-confirmation
ORMはテーブルレコードをオブジェクトにマッピングしたPHPクラスも生成します。
$ symfony propel:build-model
propel:build-model タスクはlib/modelディレクトリにデータベースと情報をやりとりするためのPHPファイルを生成します。
生成されたファイルを見ることで、Propelが1テーブルから4つのクラスを生成することにおそらく気付くでしょう。jobeet_jobテーブルを例に挙げます。
- JobeetJob
- このクラスのオブジェクトはjobeet_jobテーブルの1つのレコードを意味します。デフォルトでは空です。
- BaseJobeetJob
- JobeetJobクラスの親クラスです。propel:build-modelが実行されるたびに上書きされます。だからカスタマイズは全てJobeetJobクラスで行わなければなりません。
- JobeetJobPeer
- スタティックメソッドが定義されており、そのほとんどがJobeetJobオブジェクトを返り値です。デフォルトでは空です。
- BaseJobeetJobPeer
- JobeetJobPeerクラスの親クラスです。propel:build-modelが実行されるたびに上書きされます。だからカスタマイズは全てJobeetJobPeerクラスで行わなければなりません。
レコードのカラム値はアクセサ(get*()メソッド)やミューテータ(set*()メソッド)を使ってモデルオブジェクトで操作されます。
<?php $job = new JobeetJob(); $job->setPosition('Web developer'); $job->save(); echo $job->getPosition(); $job->delete();
オブジェクトと一緒に直接リンクする外部キーを定義できます。
<?php $category = new JobeetCategory(); $category->setName('Programming'); $job = new JobeetJob(); $job->setCategory($category);
propel:build-all タスクはこの章で行ったタスクを一括してやってくれるショートカットです。また、formsやvalidatorsのJobeetモデルオブジェクトの生成もします。
$ symfony propel:build-all
今日の最後にアクションにおけるvalidatorを見ることができます。formsについては10日目にもっと詳しく説明する予定です。
propel:build-all-load タスクはpropel:build-allを実行した後にpropel:data-loadを実行するショートカットです。
あとで見ることになりますが、symfonyはPHPクラスをオートロードします。このことはコード内でrequireを使う必要がないということを意味してます。symfonyが開発者のために自動でやってくれるのはたくさんの事柄の1つです。しかし1点よくないところもあります。それはsymfonyに新しいクラスを追加したときはキャッシュをクリアする必要があることです。propel:build-modelはたくさんのクラスを生成するのでキャッシュをクリアしましょう。
$ symfony cache:clear
初期データ
データベースにテーブルが作成されました、しかしデータがありません。Webアプリケーションでは3種類のデータがあります。
- 初期データ
- アプリケーションを動作させるのに必要なデータ。例えば、Jobeetだとカテゴリーが必要となります。もしカテゴリーが無ければ誰も仕事を投稿できなくなります。backendにログインできるadminユーザが必要になります。
- テストデータ
- アプリケーションのテストに必要です。開発者にとって、ストーリー通りにJobeetが動作するのを確実にするためにテストを書きます。自動化されたテストを書くのが1番良い方法です。テストを動かすたびにテストデータでデータベースをきれいにする必要があります。
- ユーザデータ
- アプリケーションが普通の状態でユーザによって作られたデータ
symfonyがデータベースにテーブルを作るたびに全てのデータは失くなります。初期データをデータベースに登録させるにはPHPで生成するか、mysqlからSQLを実行させます。しかしありふれた要求として、symfony内で行うのが1番良い方法です。data/fixturesディレクトリにYAMLファイルを生成し、propel:data-load タスクを使ってデータベースへロードします。
# data/fixtures/010_categories.yml JobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator } # data/fixtures/020_jobs.yml JobeetJob: job_sensio_labs: category_id: programming type: full-time company: Sensio Labs logo: /uploads/jobs/sensio_labs.png url: http://www.sensiolabs.com/ position: Web Developer location: Paris, France description: | You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_sensio_labs email: job@example.com expires_at: 2010-10-10 job_extreme_sensio: category_id: design type: part-time company: Extreme Sensio logo: /uploads/jobs/extreme_sensio.png url: http://www.extreme-sensio.com/ position: Web Designer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in. Voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_extreme_sensio email: job@example.com expires_at: 2010-10-10
fixtureファイルはYAMLで記述され、ユニーク名がラベルされたモデルオブジェクトを定義します。このラベルはプライマリキーを定義すること無しに関連オブジェクトをリンクするのに大いに役に立ちます。例えば、job_sensio_labsのカテゴリーはprogrammimngで、それは"Programming"カテゴリーから得ることができます。
fixtureファイルは1つのオブジェクトかいくつかのオブジェクトを含みます。
ファイル名のprefixに数字がついてるのに気づいてください。これはデータロードの順番をコントロールする簡単な方法です。より最近のプロジェクトでいくつかの新しいfixtureファイルを挿入することになったなら、現在使ってる番号の間で番号付けをしてやるだけで良いです。
fixtureファイルでは全てのカラム値を定義する必要はありません。もしそうでないなら、symfonyはデータベーススキーマで定義されてるデフォルト値を使います。symfonyはデータベースへデータをロードさせるのにPropelを使うので、ビルトイン関数を使ったり動作しているモデルクラスにカスタム関数を追加してもよいです。
データベースに初期データをロードするには単にpropel:data-loadタスクを実行するだけです。
$ symfony propel:data-load
ブラウザ上での動作確認
たくさんのCLIを使いますが、あんまり面白いものではありません。とりわけWebプロジェクトにとっては。データベースと情報をやり取りするWebページを作ることができます。
仕事のリストの表示や編集、削除の方法を見てみましょう。1日目で説明しましたが、symfonyはアプリケーションで構成されます。各アプリケーションはモジュールから構成されます。モジュールはアプリケーションの要素を記述したPHPコードやユーザが使うモデルオブジェクトから構成されます。
symfonyは自動で基本要素を操作できるモジュールを作ることができます。
symfony propel:generate-module --with-show --non-verbose-templates frontend job JobeetJob
仕事の編集を試したいなら、symfonyはカテゴリーのテキスト表示が必要となるのでExceptionが出るでしょう。PHPでオブジェクトの表現は__toString()というマジックメソッドを使って定義できます。カテゴリーレコードのテキスト表示はJobeetCategoryモデルクラスで定義します。
<?php // lib/model/JobeetCategory.php class JobeetCategory extends BaseJobeetCategory { public function __toString() { return $this->getName(); } }
これでカテゴリーのテキスト表現をするたびに__toString()メソッドが呼ばれてカテゴリー名が返されます。他のモデルクラスでも必要となるので全てのモデルに対して__toString()を定義しましょう。
<?php // lib/model/JobeetJob.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation()); } } // lib/model/JobeetAffiliate.php class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this->getUrl(); } }
これで仕事の作成と編集が使えるようになりました。必須項目を空にして保存しようとすると、symfonyはデータベーススキーマから基本バリデーションルールを作ります。