add to hatena hatena.comment (1) add to del.icio.us (0) add to livedoor.clip (0) add to Yahoo!Bookmark (0) Total: 1

DAY2: データモデルの作成

昨日はsymfonyのセットアップと画面をブラウザで表示させるところまでいきました。
今日はアプリケーションで使用するデータモデルを作成していきます。

ところでaskeetって何?

askeetとはQ&Aサイトです。質問を投稿し、それに答えて、"イイネ"をつけるようなアプリケーションです。

データモデルの設計

以下の図のようなテーブル設計になっています。

question: 質問内容
answer: 回答内容
user: ユーザー
interest: userテーブルとquestionテーブルとの多対多のリレーションテーブル
relevancy: userテーブルとanswerテーブルとの多対多のリレーションテーブル

symfonyでテーブルを設計する場合はレコードの作成日、更新日はcreated_at, updated_atにするルールがあります。
必須ではないのですが、このルールに従っておけばORマッパーが自動的に判断してくれるのです。

schema.xml

本家ではschema.xmlを利用してテーブル定義を書き、そのスキーマファイルから物理的なテーブルを作成しています。
ref: http://www.symfony-project.org/askeet/1_0/en/2#%3Ccode%3Eschema.xml%3C/code%3E

しかし、ここではこの方法を用いずに、物理的なテーブルを最初に作成する方法を紹介します。
本家のようにスキーマファイルを最初から書かないメリットは何でしょうか?

それは、既に身に付けているSQLの知識さえあれば良く、スキーマの構文を覚える必要がないということです。
もちろんスキーマファイルはSQLよりも短く書く事ができます。しかし、phpMyAdminなどを利用すれば単純なミスから解放されるでしょう。

というわけで、スキーマファイルを意識せずにデータモデルを作成したいと思います。

phpMyAdminでテーブル設計

まず今回のデータベースの接続情報は以下のようになっています。

DB: MySQL
Database: askeet
User: yourname
Password: yourpasswod

ユーザーとデータベースを作成しておきます。

mysql> grant all privileges on askeet.* to yourname@"%" identified by 'yourpassword';
Query OK, 0 rows affected (0.00 sec)

mysq> create database askeet;
Query OK, 1 row affected (0.00 sec)

次にテーブルのcreate文をphpMyAdminで作成しておきます。
今回は実際に作成したテーブルのcreate文をエクスポートしたものが下記になります。

これを利用する場合はSQLをdata/sql/create.sqlとして保存しておきます。

$ mkdir -p /home/sfprojects/askeet/data/sql/
$ vim /home/sfprojects/askeet/data/sql/create.sql

SQL:
  1. -- phpMyAdmin SQL Dump
  2. -- version 2.11.5.1
  3. -- http://www.phpmyadmin.net
  4. --
  5. -- ホスト: localhost
  6. -- 生成時間: 2008 年 10 月 21 日 03:54
  7. -- サーバのバージョン: 5.0.67
  8. -- PHP のバージョン: 5.2.6
  9.  
  10. SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
  11.  
  12. --
  13. -- データベース: `askeet`
  14. --
  15.  
  16. -- --------------------------------------------------------
  17.  
  18. --
  19. -- テーブルの構造 `ask_answer`
  20. --
  21.  
  22. CREATE TABLE IF NOT EXISTS `ask_answer` (
  23.   `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  24.   `question_id` int(10) UNSIGNED NOT NULL,
  25.   `user_id` int(10) UNSIGNED NOT NULL,
  26.   `body` text NOT NULL,
  27.   `created_at` datetime NOT NULL,
  28.   PRIMARY KEY  (`id`),
  29.   KEY `question_id` (`question_id`,`user_id`)
  30. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
  31.  
  32. --
  33. -- テーブルのデータをダンプしています `ask_answer`
  34. --
  35.  
  36. -- --------------------------------------------------------
  37.  
  38. --
  39. -- テーブルの構造 `ask_interest`
  40. --
  41.  
  42. CREATE TABLE IF NOT EXISTS `ask_interest` (
  43.   `question_id` int(10) UNSIGNED NOT NULL,
  44.   `user_id` int(10) UNSIGNED NOT NULL,
  45.   `created_at` datetime NOT NULL,
  46.   PRIMARY KEY  (`question_id`,`user_id`),
  47.   KEY `user_id` (`user_id`)
  48. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  49.  
  50. --
  51. -- テーブルのデータをダンプしています `ask_interest`
  52. --
  53.  
  54. -- --------------------------------------------------------
  55.  
  56. --
  57. -- テーブルの構造 `ask_question`
  58. --
  59.  
  60. CREATE TABLE IF NOT EXISTS `ask_question` (
  61.   `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  62.   `user_id` int(10) UNSIGNED NOT NULL,
  63.   `title` text NOT NULL,
  64.   `body` text NOT NULL,
  65.   `created_at` datetime NOT NULL,
  66.   `updated_at` datetime NOT NULL,
  67.   PRIMARY KEY  (`id`),
  68.   KEY `user_id` (`user_id`)
  69. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
  70.  
  71. --
  72. -- テーブルのデータをダンプしています `ask_question`
  73. --
  74.  
  75. -- --------------------------------------------------------
  76.  
  77. --
  78. -- テーブルの構造 `ask_relevancy`
  79. --
  80.  
  81. CREATE TABLE IF NOT EXISTS `ask_relevancy` (
  82.   `answer_id` int(10) UNSIGNED NOT NULL,
  83.   `user_id` int(10) UNSIGNED NOT NULL,
  84.   `score` int(11) NOT NULL,
  85.   `created_at` datetime NOT NULL,
  86.   PRIMARY KEY  (`answer_id`,`user_id`),
  87.   KEY `user_id` (`user_id`)
  88. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  89.  
  90. --
  91. -- テーブルのデータをダンプしています `ask_relevancy`
  92. --
  93.  
  94. -- --------------------------------------------------------
  95.  
  96. --
  97. -- テーブルの構造 `ask_user`
  98. --
  99.  
  100. CREATE TABLE IF NOT EXISTS `ask_user` (
  101.   `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  102.   `nickname` varchar(50) NOT NULL,
  103.   `first_name` varchar(100) DEFAULT NULL,
  104.   `last_name` varchar(100) DEFAULT NULL,
  105.   `created_at` datetime NOT NULL,
  106.   PRIMARY KEY  (`id`),
  107.   KEY `nickname` (`nickname`)
  108. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
  109.  
  110. --
  111. -- テーブルのデータをダンプしています `ask_user`
  112. --
  113.  
  114. --
  115. -- ダンプしたテーブルの制約
  116. --
  117.  
  118. --
  119. -- テーブルの制約 `ask_answer`
  120. --
  121. ALTER TABLE `ask_answer`
  122.   ADD CONSTRAINT `ask_answer_ibfk_1` FOREIGN KEY (`question_id`) REFERENCES `ask_question` (`id`);
  123.  
  124. --
  125. -- テーブルの制約 `ask_interest`
  126. --
  127. ALTER TABLE `ask_interest`
  128.   ADD CONSTRAINT `ask_interest_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `ask_user` (`id`),
  129.   ADD CONSTRAINT `ask_interest_ibfk_1` FOREIGN KEY (`question_id`) REFERENCES `ask_question` (`id`);
  130.  
  131. --
  132. -- テーブルの制約 `ask_question`
  133. --
  134. ALTER TABLE `ask_question`
  135.   ADD CONSTRAINT `ask_question_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `ask_user` (`id`);
  136.  
  137. --
  138. -- テーブルの制約 `ask_relevancy`
  139. --
  140. ALTER TABLE `ask_relevancy`
  141.   ADD CONSTRAINT `ask_relevancy_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `ask_user` (`id`),
  142.   ADD CONSTRAINT `ask_relevancy_ibfk_1` FOREIGN KEY (`answer_id`) REFERENCES `ask_answer` (`id`);

このcreate.sqlでテーブルを作成する場合は以下のようにすればできるのは周知の内容です。

$ mysql -uyourname -p askeet < /home/sfprojects/askeet/data/sql/create.sql

さぁ、実際のテーブルからデータモデルのコードを自動生成させましょう。

ここで大きな2つの選択肢があります。
それはORマッパーとしてPropelかDoctrineのどちらかを使う事ができるからです。

symfony1.1まではPropelが標準でしたがDoctrineが直感的に書ける点で人気があります。
本家ではPropelを使っているのでここではDoctrineを使う事にしましょう。

これらのORマッパーはプラグインとして提供されています。
そのため、プラグインの有効無効を設定する必要があります。

プラグインの設定は ./config/ProjectConfiguration.class.php で行います。

$ vim ./config/ProjectConfiguration.class.php

PHP:
  1. public function setup()
  2. {
  3.       $this->enablePlugins('sfDoctrinePlugin');
  4.       $this->disablePlugins('sfPropelPlugin');
  5. }

次にsymfonyに接続情報を記述しなくてはいけません。
接続情報は./config/database.ymlに記述します。

ここでyml(ヤムル)ファイルという種類のファイルがあることがわかります。
これはXMLよりも可読性の高い構造を表すことができるファイルです。

詳しくは下記サイトを見ると数十分で理解できるでしょう。

ref: http://jp.rubyist.net/magazine/?0009-YAML

では、設定をかきましょう。

$ vim ./config/database.yml

C:
  1. all:
  2.   main:
  3.     class: sfDoctrineDatabase
  4.     param:
  5.       dsn: 'mysql:host=localhost;dbname=askeet'
  6.       username: yourname
  7.       password: yourpassword

さぁ、これで準備ができました。
さっそくデータベースからスキーマファイルを自動生成させてみましょう。
どのコマンドを叩くのか解らない場合はlistコマンドで確認できます。

doctrineで用意されているタスクをlist doctrineコマンドで確認してみます。

$ ./symfony list doctrine

:build-all Generates Doctrine model, SQL and initializes the database (doctrine-build-all)
:build-all-load Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-load)
:build-all-reload Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-reload)
:build-all-reload-test-all Generates Doctrine model, SQL, initializes database, load data and run all test suites (doctrine-build-all-reload-test-all)
:build-db Creates database for current model (doctrine-build-db)
:build-filters Creates filter form classes for the current model
:build-forms Creates form classes for the current model (doctrine-build-forms)
:build-model Creates classes for the current model (doctrine-build-model)
:build-schema Creates a schema from an existing database (doctrine-build-schema)
:build-sql Creates SQL for the current model (doctrine-build-sql)
:data-dump Dumps data to the fixtures directory (doctrine-dump-data)
:data-load Loads data from fixtures directory (doctrine-load-data)
:dql Execute a DQL query and view the results (doctrine-dql)
:drop-db Drops database for current model (doctrine-drop-db)
:generate-migration Generate migration class (doctrine-generate-migration)
:generate-migrations-db Generate migration classes from existing database connections (doctrine-generate-migrations-db, doctrine-gen-migrations-from-db)
:generate-migrations-models Generate migration classes from an existing set of models (doctrine-generate-migrations-models, doctrine-gen-migrations-from-models)
:generate-module Generates a Doctrine module (doctrine-generate-crud, doctrine:generate-crud)
:generate-module-for-route Generates a Doctrine module for a route definition
:init-admin Initializes a Doctrine admin module (doctrine-init-admin)
:insert-sql Inserts SQL for current model (doctrine-insert-sql)
:migrate Migrates database to current/specified version (doctrine-migrate)
:rebuild-db Creates database for current model (doctrine-rebuild-db)

今回利用するのはbuild-schema, build-modelです。

$ ./symfony doctrine:build-schema
>> doctrine generating yaml schema from database

これで./config/doctrine/schema.ymlが作成されました。
askeetのチュートリアルでは実際のテーブル名とモデル名が異なります。
テーブル名にはask_という接頭辞がついています。

そこで、./config/doctrine/schema.ymlで各テーブルのモデル名を変更しておきます。

$ vim ./config/doctrine/schema.yml

ここで先頭のモデル名を

C:
  1. AskInterest:
  2.   tableName: ask_interest
  3.   columns:

から

C:
  1. Interest:
  2.   tableName: ask_interest
  3.   columns:

とAsk部分を取り除いておきます。

また、MySQLのTEXTタイプがstring(2146483647)に解釈されています。
このままですとフォームを自動生成させたときにtextareaとして認識されませんので
clobに変更しておきます。

以下のようなschema.ymlが完成形です。

C:
  1. Interest:
  2.   tableName: ask_interest
  3.   columns:
  4.     question_id:
  5.       type: integer(4)
  6.       unsigned: 1
  7.       primary: true
  8.     user_id:
  9.       type: integer(4)
  10.       unsigned: 1
  11.       primary: true
  12.     created_at:
  13.       type: timestamp(25)
  14.       default: ''
  15.       notnull: true
  16.   relations:
  17.     User:
  18.       local: user_id
  19.       foreign: id
  20.       type: one
  21.     Question:
  22.       local: question_id
  23.       foreign: id
  24.       type: one
  25. Question:
  26.   tableName: ask_question
  27.   columns:
  28.     id:
  29.       type: integer(4)
  30.       unsigned: 1
  31.       primary: true
  32.       autoincrement: true
  33.     user_id:
  34.       type: integer(4)
  35.       unsigned: 1
  36.       default: ''
  37.       notnull: true
  38.     title:
  39.       type: clob
  40.       default: ''
  41.       notnull: true
  42.     body:
  43.       type: clob
  44.       default: ''
  45.       notnull: true
  46.     created_at:
  47.       type: timestamp(25)
  48.       default: ''
  49.       notnull: true
  50.     updated_at:
  51.       type: timestamp(25)
  52.       default: ''
  53.       notnull: true
  54.   relations:
  55.     User:
  56.       local: user_id
  57.       foreign: id
  58.       type: one
  59. User:
  60.   tableName: ask_user
  61.   columns:
  62.     id:
  63.       type: integer(4)
  64.       unsigned: 1
  65.       primary: true
  66.       autoincrement: true
  67.     nickname:
  68.       type: string(50)
  69.       default: ''
  70.       notnull: true
  71.     created_at:
  72.       type: timestamp(25)
  73.       default: ''
  74.       notnull: true
  75.     first_name: string(100)
  76.     last_name: string(100)
  77. Relevancy:
  78.   tableName: ask_relevancy
  79.   columns:
  80.     answer_id:
  81.       type: integer(4)
  82.       unsigned: 1
  83.       primary: true
  84.     user_id:
  85.       type: integer(4)
  86.       unsigned: 1
  87.       primary: true
  88.     score:
  89.       type: integer(4)
  90.       default: ''
  91.       notnull: true
  92.     created_at:
  93.       type: timestamp(25)
  94.       default: ''
  95.       notnull: true
  96.   relations:
  97.     User:
  98.       local: user_id
  99.       foreign: id
  100.       type: one
  101.     Answer:
  102.       local: answer_id
  103.       foreign: id
  104.       type: one
  105. Answer:
  106.   tableName: ask_answer
  107.   columns:
  108.     id:
  109.       type: integer(4)
  110.       unsigned: 1
  111.       primary: true
  112.       autoincrement: true
  113.     question_id:
  114.       type: integer(4)
  115.       unsigned: 1
  116.       default: ''
  117.       notnull: true
  118.     user_id:
  119.       type: integer(4)
  120.       unsigned: 1
  121.       default: ''
  122.       notnull: true
  123.     body:
  124.       type: clob
  125.       default: ''
  126.       notnull: true
  127.     created_at:
  128.       type: timestamp(25)
  129.       default: ''
  130.       notnull: true
  131.   relations:
  132.     Question:
  133.       local: question_id
  134.       foreign: id
  135.       type: one

ではモデルを作成しましょう。

$ ./symfony doctrine:build-model
>> doctrine generating model classes

これで ./lib/model/doctrine以下に今後修正を加えるファイルが作成されました。
また./lib/model/doctrine/generated以下にもファイルが作成されますが、これはコマンドによって上書きされるファイルです。
このファイルには修正を加えてはいけません。忘れないようにしておきましょう。

フォームクラスを生成する

symfony1.1からはフォームフレームワークが新しく導入されました。
これは以前までyamlやアクションで設定していたフォームに関する処理を委託できるフレームワークです。

綺麗にソースがまとまる反面コーディング方法や内容について理解する必要があるため、
最初は大変かもしれません。ここでは深く触れずに動く物を作ってみます。

モデルの生成と同じで、基底クラスはsymfonyのコマンドで生成することができます。
ではさっくりと作ってしまいましょう。

$ ./symfony doctrine:build-form
>> doctrine generating form classes

作成されたファイルは./lib/form/doctrine以下に生成されます。

CRUDでデータアクセスしてみる

CRUDとはWebアプリケーションで一般的な操作である
Create,Read,Update,Deletenの操作を意味します。

これらの操作の画面をデータモデルから作成するのがCRUDの自動作成になります。

以下のコマンドで簡単にできてしまいます。

$ ./symfony doctrine:generate-module frontend quetion Question

これで,ask_questionを操作するlist, edit. show, index, update, deleteアクションが自動生成されました。
なんとも簡単ですね!

では実際に画面を見てみましょう。
URLはアプリケーション名、モジュール名、アクション名(省略時はindex)になります。

http://symfony.centos5.localhost/frontend_dev.php/question

しかし、ローカル環境でうごかしていない場合は以下のようなエラーが画面に表示されてとまってしまいます。

You are not allowed to access this file. Check frontend_dev.php for more information.

これは、_dev.phpファイルは様々なデバッグ情報を表示するために呼び出すコントローラーなので、
誤って本番にリリースしても動作しないようにIPアドレスでの制限がかかっています。

これを解除する場合はコントローラーファイルを編集し、IPアドレスの制限を外すかIPアドレスを追加します。

$ vim ./web/frontend_dev.php

私の環境では 172.16.203.1で開発サーバーにアクセスするのでこのIPアドレスを以下のように追加しました。

PHP:
  1. if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1', '172.16.203.1')))
  2. {
  3.   die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
  4. }

では、画面をみてみましょう!


残念なことにnot nullの項目があるため、データを登録することはできません。

ここまでのソースは
http://svn.1ms.jp/public/symfony/askeet12/tags/release_day_2

から取得することができます。

では、また明日。

関連するその他の記事

Comments

Leave a Reply