http://yuubiseiharukana.blog.shinobi.jp/Entry/158/の続き?
CakePHPの基本は、ひとつのコントローラにひとつのテーブルです。
つまり、その状態ではリレーショナルをまともに扱うことができません。
複数のテーブルを扱いたい場合、実現には幾つかの手段があります。
単純に結合で話が済む場合は、モデルを記述することで解決されます。
とりあえず結合を使うようなテーブルを作成。
CREATE TABLE `books` ( |
とりあえず適当に作っただけなので各テーブルにはあまり意味はありません。
各テーブルに何件か適当にデータを入れておきます。
モデルに、他のモデルとの関係を記述していきます。
モデルとして読み込もうとしているBooksモデルの記述は必要ですが、そこからリンクされているAuthorsモデルやBooks_supplementationsモデルはとりあえず記述する必要はありません。
app/models/book.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class Book extends AppModel {
//自分の名前
var $name = 'Book';
//books.author_idはauthors.idに外部キー
//books.publisher_idはpublishers.idに外部キー
//books.idはbooks_supplementations.book_idから外部キー
//といった関係を記述しておくとfind等で自動的に結合する
var $belongsTo = array(
'Author' =>array(
'className' => 'Author',
'foreignKey'=> 'author_id'
)
,'Publisher' =>array(
'className' => 'Publisher',
'foreignKey'=> 'publisher_id'
)
);
var $hasMany = array(
'Books_supplementation' =>
array(
'className' => 'Books_supplementation',
'foreignKey'=> 'book_id'
)
);
}
|
記述がめんどくさいんですが、簡単に言うとhasは相手側テーブルから自分に対して外部キーが張ってある、belongsToは自分から相手側に外部キーが張ってあるということです。
Bookモデルの場合、authorsテーブルとpublishersテーブルに対して外部キーが張ってあり、books_supplementationsテーブルから外部キーを張られています。
その後コントローラ内で
$this->Book->find(array('Book.id <>'=>'2'));
などとすると、Bookモデル内の結合条件を勝手に読み取って勝手にJOINなんかを付け加えてSELECTしてきます。
結果のSQLはこんな感じに。
見事に表示できま…一件しか出てこない。
SELECT (略) FROM `books` AS `Book`
LEFT JOIN `authors` AS `Author` ON (`Book`.`author_id` = `Author`.`id`)
LEFT JOIN `publishers` AS `Publisher` ON (`Book`.`publisher_id` = `Publisher`.`id`)
WHERE `Book`.`id` <> 2
LIMIT 1
CakePHPの記事一覧
2009/10/30追記
findは結果セットを1件だけ取得するメソッドで、全件取得したい場合はfindAllを使用します。
詳細は次の記事を参照。
コメントでの指摘ありがとうございます。
Zend_Db_SelectはSELECT文を作成するクラスです。
前回は自力でSQL文を書いていましたが、抽出するカラムや抽出条件などをSQLを直接書くこと無しに作成することができます。
zend_db2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
//Zend_Form
require_once('Zend/Db.php');
$form = new Zend_Db();
//DBインスタンス
$db=Zend_Db::factory(
'Pdo_Mysql'
,array(
'host'=>'localhost'
,'username'=>'testuser'
,'password'=>'testpass'
,'dbname'=>'zend_test'
,'profiler'=>true
)
);
$db->getProfiler()->setEnabled(true);
//Zend_Db_Selectインスタンス取得
$select=new Zend_Db_Select($db);
// SELECT * FROM books
$select->from('books');
// LEFT JOIN author ON author.id=books.author_id
$select->joinLeft('author','author.id=books.author_id');
// WHERE books.id=1
$select->where('books.id = ?',1);
// ORDER BY books.id DESC
$select->order('books.id DESC');
// LIMIT 1
$select->limit(1);
/*
メソッドチェーンも可能
$select->from('books')
->joinLeft('author','author.id=books.author_id')
->where('books.id = ?',1)
->order('books.id DESC')
->limit(1);
*/
//取得は何故かメソッドチェーンできない
$query=$select->query();
$books=$query->fetchAll();
|
このようにメソッドを繋げることで、最終的に
SELECT `books`.*, `author`.* FROM `books` LEFT JOIN `author` ON author.id=books.author_id WHERE (books.id = 1) ORDER BY `books`.`id` DESC LIMIT 1
というSQLが完成します。
またZend_Db_Selectはメソッドチェーンに対応しており、コメントアウトしてあるようなかんじで一気にメソッドを組み立てることもできます。
まあここらへんは好きな方で書けばいいでしょう。
ただ、fromやjoinなんかはほいほいメソッドチェーン繋げられるのに、何故か結果の取得だけはメソッドチェーンできません。
$Zend_Db_Select->query->fetchOne()
なんてことができず、一旦queryを発行してZend_Db_Statementオブジェクトを受け取った後fetchしなければなりません。
残念。
今回は固定クエリなのであまりメリットが見えませんが、検索条件が変動する場合などに威力を発揮します。
SQLをわざわざ組み立てる必要がなく、whereメソッドを追加したりしなかったりするだけでいいので作成が楽です。
XAMPP1.7.1を入れたのですが、デフォルト状態だといつものように文字化けが発生します。
"MYSQL 文字化け"でググるとmy.cnfが腐るほど出てくるわけですが、今回文字化けを直そうとmy.cnfを探すも何処にも見あたらない。
いつのまにか設定ファイルがC:\xampp\mysql\bin\my.iniになってました。
いつから変更になったのかよくわかりませんが、とりあえずこれでWindows環境でmy.cnfが見つからないという質問は無くなりそうですね。
まあmy.cnf自体が無くなってるのでやっぱり質問が増えるかもしれませんが。
ちなみに正しい設定ファイル置き場はコマンドプロンプトから
C:\xampp\mysql\bin>mysql -?
で見ることができます。
C:\WINDOWS\my.ini C:\WINDOWS\my.cnf C:\my.ini C:\my.cnf C:\xampp\mysql\my.ini C:\xampp\mysql\my.cnf
というふうになっていました。
前にあるのを優先して読むようなので、うっかり変なファイルがあったら動きがおかしくなったりします。
というわけでmy.iniを編集してみました。
とりあえず[mysqld]セクションに下記追加して文字化け解消。
character-set-server = utf8
collation-server = utf8_general_ci
init-connect=SET NAMES utf8
……と思ったが微妙にうまくいかない。
せっかく設定したcharacter-set-serverがMySQLを再起動するたびに効いたり効かなかったりするんだがなんだこれ。
さて、現在起動しているサービスから起動しているmysqlの状態を確認することができます。
C:\xampp\mysql\bin\mysqld.exe --defaults-file=c:\xampp\mysql\bin\my.cnf mysql
my.cnfなんて無いよ!
なんだよこの起動オプション。
インストール時にいつの間にやらこんな起動オプションが設定されてしまっていたみたいです。
これではせっかくのmy.iniが読み込まれてくれません。
仕方ないのでmy.iniをmy.cnfにコピペしたらあっさり文字化けが直った。
どうしてこうなった?
前回の続き。
app/models/cake_sample.phpを作成します。
中身はとりあえずダミーのCakeSampleモデルを作成しておきます。
models/cake_sample.php
1
2
3
4
5
6
|
class CakeSample extends AppModel {
//自分の名前?
var $name = 'CakeSample';
}
|
その後CakeSampleControllerコントローラ内で$this->CakeSampleって指定すると勝手にCakeSampleクラスがインクルードされて使用されます。
CakeSampleモデルの親クラスAppModelに最初からある程度メソッドが揃っているので、これを使用してDBの中のcake_samplesテーブルが読めるようになるみたいです。
次にコントローラにモデルを読み出す処理を記入。
controllers/cake_sample_controller.php
1
2
3
4
5
6
7
8
9
10
|
class CakeSampleController extends AppController{
public function TestAction() {
//CakeSampleモデルから中身を全部取得
$this->set('cake_sample', $this->CakeSample->find('all'));
}
}
|
setした変数そのものはビューからその名前で呼び出せます。
views/cake_sample/test_action.ctp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
print('<table>');
foreach($cake_sample as $key=>$val){
print('<tr><td>');
print($val['CakeSample']['id']);
print('</td><td>');
print($val['CakeSample']['title']);
print('</td><td>');
print($val['CakeSample']['body']);
print('</td><td>');
print($val['CakeSample']['created']);
print('</td></tr>');
}
print('</table>');
|
たったこれだけでデータベースの内容が表示できました。
とりあえずどのファイルがどうやって読み込まれるかとか何処に何のインスタンスが出来るかとかが全然分かってないので非常に気持ち悪い。
CakePHPの記事一覧
CakePHP1.2.4.8284
前触ったときはどうにか動く程度になったところでわからずに止めてしまったので再挑戦。
まずはダウンロードして解凍。
RewriteEngineを使用しているのでmod_rewriteを有効にする。
適当なフォルダに丸ごとアップ。
で、フォルダを覗いてみるといきなり動作しています。
個別の設定をしていないのでデフォルト状態であり、DBも設定していないので書き込みなどは行えません。
色々注意が現れます。
Notice (1024): Please change the value of 'Security.salt'
Your database configuration file is NOT present
まずSecurity.saltの設定。
ユーザパスワードをDBに保存するときは当然暗号化を行いますが、その暗号化に利用する値です。
デフォルトだと解読されてしまう可能性があるので適当に変えてしまいます。
app/config/core.phpの150行目あたり、
Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');
を適当な値に変更しましょう。
うっかり運用開始後に変更してしまうと認証できなくなるので注意です。
次にDBの設定。
app/config/database.php.defaultをdatabase.phpにコピー、DATABASE_CONFIGクラスの各値を適切にセットします。
persistentは持続的接続を使用するかどうか、わからなければfalseのままにしておきます。
あとapp/tmp/フォルダをPHPから読み書き可能にする設定も必要ですが、まあWindowsでは関係ないです。
画面から黄色や赤色が無くなれば設定は終了。次に進みます。
CakePHP最大の欠点として、命名規則がややこしいということがあります。
モデル名をCakeSampleとしたら、コントローラ名はCakeSamplesControllerとしなければならないしDBテーブル名はcake_samplesとしなければなりません。
なんでわざわざ複数形とかキャメルケースとか混乱させるんだ。
というわけでDB用のテーブルとしてcake_samplesを作成します。
CREATE TABLE `cake_samples` ( `id` int(10) unsigned NOT NULL auto_increment, `title` varchar(50) default NULL, `body` text, `created` datetime default NULL, `modified` datetime default NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO cake_samples (title,body,created) VALUES ('タイトル', '本文', NOW()); |
idとcreatedとmodifiedはCakePHPが要求するので何はなくとも入れておきます。
適当に何件か登録しておきましょう。
次にコントローラを作成します。
コントローラは簡単に言うとURLによる処理の振り分けです。
正直、更新やら分岐やら何処に書けばいいのかよくわかんないんですよね。
まあ適当にやってみます。
まず
http://localhost/CakeSample/TestAction/a/b
にアクセスすると
Error: CakeSampleController could not be found.
みたいなエラーが表示されます。
ぶっちゃけ書いてあるとおりにすればいいだけなのですが、とりあえずファイルとコントローラを作ります。
/app/controllers/にcake_sample_controller.phpを作成、CakeSampleControllerという名前のクラスを作成します。
中身はとりあえず空っぽ。
またエラー。
Error: The action TestAction is not defined in controller CakeSampleController
TestActionを作れ、と言われます。
要するに上記URLのうち、
ルートから最初の部分(CakeSample)がコントローラ名として、その次の部分(TestAction)がアクション名として呼び出されることになります。
実際の中身としては
http://localhost/?url=CakeSample/TestAction/a/b
と同じで、mod_rewriteによって見た目を変更しているだけです。
このような作りはZendFrameworkやSymphonyといった多くのフレームワークで採用されており、URLがわかりやすく検索エンジンにも取得されやすいといった利点があります。
というわけでCakeSampleControllerにTestActionを作成します。
こちらも中身は空。
次はビューのエラー。
Error: The view for CakeSampleController::TestAction() was not found.
エラーメッセージに出てくるとおりに
/app/views/cake_sampke/test_action.ctp
にファイルを作成します。
中身はやはり空。
以上でとりあえずエラーが出なくなりました。
何も書いていないのに、CakePHPのデフォルトで用意されているテンプレートが表示されます。
ちなみにこの後1.2.5をインストールしてみたのですが、一番上に堂々と「Release Notes for CakePHP 1.2.4.8284.」の文字が。
真っ先に気付く場所だろそこ。
CakePHPの記事一覧
http://openpear.org/package/Acme_IdolMaster
どうにかした。
http://openpear.org/package/Acme_MorningMusume
を参考にやってみようと思ったら、privateをextendsしてるせいで同じプロパティが階層別にできてて、そのせいでBASEに__getを書いても値が取得できなくて超困ったので勝手にprotectedにした。
こういうときの作法がどっかにあるのかもしれないけど全然知らないので超俺俺実装。
ということでとりあえず動くようになりました。
もし文法とか駄目だったら誰かが直してくれるでしょう。きっと。
使用はこんな感じ。
ちょっとだけイテレータやメソッドチェーンも使えるようにしてみた。
Acme_IdolMaster.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
//インスタンス作成
require_once('Acme/IdolMaster.php');
$imas=new Acme_IdolMaster();
//全メンバーをループ
foreach($imas as $key=>$val){
$member_name[]=$val->family_name_ja;
}
//りっちゃんを取得
$ricchan=$imas->get('AkizukiRitsuko');
$ricchan=$imas->AkizukiRitsuko;
$ricchan=$imas->AkizukiRitsuko();
//りっちゃんのパラメータを取得
$name[]=$imas->AkizukiRitsuko->get('family_name_ja');
$name[]=$imas->AkizukiRitsuko->family_name_ja;
$name[]=$ricchan->family_name_ja();
//りっちゃんのパラメータを変更
$imas->AkizukiRitsuko->family_name_ja('名前1');
$ricchan->family_name_ja='名前2';
$ricchan->set('family_name_ja','名前3');
//りっちゃんを指定
$imas->select('AkizukiRitsuko');
$name[]=$imas->get('family_name_ja');
$name[]=$imas->family_name_ja;
$name[]=$imas->family_name_ja();
$imas->set('family_name_ja','名前4');
$imas->family_name_ja='名前5';
$imas->family_name_ja('名前6');
//指定解除
$imas->select();
|
色々な書き方をしていますが全部同じです。
使用するときは使用法を揃えておいた方がいいと思いますが。
まあ使用するときなんて存在するのかという疑問がありますが気にしない。
んでまあ、作ったのはいいんだが管理者ではないのでリリース権限がないみたい。
使いたい場合はリポジトリから拾ってきてください。
次はAcme_Loveplusでも作ってみようか。
…ていうか、この手のシリーズ中身は同じなんだから纏められたりしないもんかね。
OpenPear-01:Acme_IdolMaster
で、
>野望だったらもうちょっとどうにかしろよ!
と書いたところ、Openpearの中の人から
>あれま 「誰でもコミットできる」と言っている.気になったら「どうにかしろ」じゃなくて自分でどうにかできる.ブログに文句を書く前にコミットをどうぞ. |
と言われた。
なんで私がそんなことしなきゃならんのだ?
http://openpear.org/package/Acme_MorningMusume
>phpcon 2008 での takesako さんの一言から openpear への野望は始まった訳です。 |
と言ってるんだから、野望というほどのものであれば普通自分でどうにかするだろ、と思ったまでのこと。
ていうかそもそも、それ以前にモー娘。(←何故か変換できる)がよくわからないので触るに触れない。
一番最後の記憶がハッピーサマーウェディングだ。
ちなみに最初の記憶はLoveマシーンで二番目の記憶がハッピーサマーウェディング。
というわけでOpenPearパッケージを作ってみた。
http://openpear.org/package/Services_Shitaraba
まあ例のしたらば削除スクリプトから各メソッドを切り出しただけというかんじなわけですが。
使い方は簡単。
リンク先にあるとおり、
>pear install openpear/Services_Shitaraba
でインストールできます。
もしくは2ファイルしかないので手動でコピペでも可。
そしたら他のPearとまったく同じ感覚で使えます。
まあ、例外?何それ?とばかりにfalse返すだけでその原因がさっぱり分からないとか、書き方がPearのコーディング規約に全く沿っていないとか、そもそもServices配下でいいんだろうかとか、色々アバウトな点もあったりしますが気にしない。
以下のように使用します。
sample.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
//OpenPear/Services/Shitaraba
require_once('Services/Shitaraba.php');
//引数はジャンル、掲示板番号、管理画面パスワード
$shitaraba=new Services_Shitaraba('gerne','123456789','password');
//各パラメータを個別にセットする場合
$shitaraba->setGerne('gerne');
$shitaraba->setBbsnum('123456789');
$shitaraba->setPw('password');
//全スレッドリストを取得
$ret=$shitaraba->getThreadList();
//特定のスレッドを取得
$ret=$shitaraba->getThread('123456789');
//全スレッドの全データを取得
$ret=$shitaraba->getAllThreadData();
//NGワードが含まれる発言を取得
$ret=$shitaraba->getNGThreadData('あああああ');
//NGワードが含まれる発言を削除
$ret=$shitaraba->deleteNGThreadData(
$shitaraba->getNGThreadData('あああああ')
);
//全発言数を指定(1000以外に変更している場合に使用)
$ret=$shitaraba->setMaxNumber(2000);
//1000(setMaxNumber)行ったスレを取得
$ret=$shitaraba->getDatThreadList();
//1000(setMaxNumber)行ったスレをdat落ちさせる
$ret=$shitaraba->datThread(
$shitaraba->getDatThreadList()
);
//特定のスレをdat落ちさせる
$ret=$shitaraba->datThread(
array(123456789,234567890)
);
|
特にNGワード削除は管理画面からだとやってらんないくらいめんどくさいので、省力化に有効な手段なのではないかと思います。
Pearは使う分には便利ですが、パッケージ作成には申請出してパッケージ定義のXML書いて審査があって検閲があってどうのと面倒です。
またCPANのAcme::のようなネタモジュールの公開も(たぶん)許されていません。
そんなわけで有志によるもうすこし緩いPearライブラリとしてOpenPearが存在します。
とりあえず適当に使ってみます。
インストールもPearと同じです。
cd C:\xampp\php
pear channel-discover openpear.org
pear install openpear/Acme_IdolMaster-alpha
なんか知らんがAcme/Amce/にインストールされてしまったのでフォルダ一個上に移動。
何故。
imas.php
1
2
3
4
5
6
7
8
9
|
//インスタンス
require_once('Acme/IdolMaster.php');
$imas=new Acme_IdolMaster();
$haruka=new Acme_IdolMaster_Member_AmamiHaruka();
//使い方が分からぬ
print('<html><pre>');var_dump($haruka,$imas);die();
|
せっかくインストールしたはいいが使い方が分からぬ。
中身を取得するメソッドが見あたらないんだがどうすれバインダー
Acme_IdolMaster_Member_Baseクラスはあるのにextendsしてないし。
さすがα版だぜ。
原型になったと思われるAcme_MorningMusumeのほうはというと、
public function select(){
throw new Exception('><');
}
じゃねえよ!
野望だったらもうちょっとどうにかしろよ!
やっぱ審査も多少は必要かも。
Zend_DbはMySQLやOracleといったDB間の差異を吸収してくれるデータベースアダプタです。
といっても内部的にPDOを使用しているのでPDOが使えるのならわざわざZend_Dbを使用する意味はほとんどないんですが。
zend_db.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
//Zend_Db
require_once('Zend/Db.php');
$form = new Zend_Db();
//DBインスタンス
$db=Zend_Db::factory(
'Pdo_Mysql'
,array(
'host'=>'localhost'
,'username'=>'testuser'
,'password'=>'testpass'
,'dbname'=>'zend_test'
,'profiler'=>true
)
);
//SQL実行
$sql="SELECT * FROM test";
$ret = $db->fetchAll($sql);
//プリペアドステートメントは非常に簡単
$num=$_REQUEST['num'] ? $_REQUEST['num'] : 1;
$sql="SELECT * FROM test WHERE id=?";
$ret = $db->fetchAll($sql,1);
//インサート
$insert_array=array(
'value'=>'あいうえお'
);
$db->insert('test', $insert_array);
//関数のインサートはちょっとだけ面倒
$insert_array=array(
'value'=>'あいうえお'
,'date'=>(new Zend_Db_Expr('NOW()'))
);
$db->insert('test', $insert_array);
//関数とエスケープ値を同時に使用したい場合はすごく面倒
$insert_array=array(
'value'=>'あいうえお'
,'date'=>new Zend_Db_Expr(
'NOW()+INTERVAL '.$db->quote($_REQUEST['hour'],'INTEGER').' HOUR'
)
);
$db->insert('test', $insert_array);
//実行したSQLとかを調査できる
$profiler = $db->getProfiler();
$query_profile=$profiler->getLastQueryProfile();
print($query_profile->getQuery());
|
便利なのはプリペアドステートメントです。
SQL文中に?を書いて、呼び出すときに第二引数に指定するだけです。
複数指定する場合は配列で指定します。
PDOにおけるbindValueより手間が少なくなります。
インサートも配列を送るだけで実現できますが、関数を使いたい場合はZend_Db_Exprを通さないといけません。
まあZend/Db.phpを呼んだ時点で使用できる分DB_DataObject_Castなんかよりよっぽどましですが。
他のDB接続モジュールと違う明白な利点は、プロファイラが充実しているところです。
インスタンス作成時に引数としてとりあえず'profiler'=>trueを渡しておけばプロファイラが起動し、以後に実行したSQLなんかを保存してくれます。
その後$db->getProfiler()でプロファイラのインスタンスを取得でき、そこから各SQLの詳細なんかを見ることができます。
バインドした値なんかも見えるので便利。
また実行時間をチェックして、実行が遅いSQLを抽出したりといったパフォーマンスチューニングに便利な機能もあります。
チューニングのために毎回microtime()とかTIMEDIFFとかを埋め込んだりする必要もなく、開発が簡単に行えます。