CakePHPのアソシエーション
CakePHPには、非常に強力なアソシエーションという機能があります。
これは相互に関連する複数のModel(テーブル)を一括して扱うための優れた仕組みです。
便利なアソシエーションですが、マニュアルを読んで理解したつもりでも、実際に活用する段になって、どのように設定すればよいのか迷うことがあります。
CakePHPに不慣れな方がアソシエーションで迷っていたときに、どのように教えてあげると分かりやすいのかという視点で、アソシエーションについてまとめてみました。
ここではHABTM(Has And BelongsTo Many)については触れません。それはまた別の機会で。
よくある勘違い
一般にAとBというふたつのモデルがあったとき、AモデルがBモデルを『ひとつ持っているときはhasOne』で『複数持っているときはhasMany』と認識している場合がありますが、その理解のままだと、ときに混乱してしまいます。
よくあるのが、以下のようなケースです。
- ブログの記事を扱ったArticleモデルと、その記事の『編集中・投稿済・削除』等のステータスを扱うStatusモデルがあり
- ArticleモデルとStatusモデルのアソシエーションを検討したとき
- Article hasOne Status と間違ってしまう
ということがあります。
記事が、ひとつのステータスを保持するわけですから、hasOneだろうと考えると、『あれ?どうやってこれ設定するんだ???』となってしまいがちです。
つまり、上記の『ひとつ持っている』『複数持っている』という考え方は、忘れたほうが良いのです。
アソシエーションの基本
それでは、まず、基本的なところからおさらいしつつ、なぜ上述のような混乱に陥るのかを説明していきたいと思います。
アソシエーションの基本原則其の一:外部キーを持っているときはbelongsTo
まず、belongsToからおさらいしましょう。
belongsToは、あるモデル(B)があるモデル(A)に属している時に、B belongsTo A とアソシエーションを設定します。
『属してるってなんぞや?』というと、簡単に言えば、『hasOneもしくはhasManyされている』ということです。
A hasOne B もしくは、A hasMany Bという関係が成り立っている場合、B belongsTo Aというアソシエーションとなります。
hasOne や hasMany が存在せず、belongsTo だけ設定した場合でも、アプリによっては何の支障もなく動作します。
しかし、ここでは基礎を学ぶという意味で、逆方向のアソシエーションの存在と『対』で理解してもらうことから始めます。
外部キー
相手方のモデルの主キー(id)を『外部キー』といい、belongsToが設定されたモデルには、必ずこのフィールドが存在します。
たとえば、上述のArticleモデルが複数のコメントを扱うCommentモデルとhasManyの関係にある場合、
- Article hasMany Comment
- Comment belongsTo Article
ですから、Commentモデルには、article_idというフィールドが必要になります。
逆に言えば、以下の原則が成り立つということです。
アソシエーションの基本原則其の二:hasOneおよびhasManyの相手方のモデルには外部キーが存在する
A hasOne B もしくは A hasMany B というアソシエーションの場合、AモデルにはBモデルの外部キーはありません。
分かりやすいhasManyの例で説明すると、上述の例で、Articleモデルにcomment_idというフィールドを設定することは不可能です。
だってコメントは複数存在できるので、フィールドがひとつでは足りませんから(笑)
hasOne も hasMany と同類
『じゃあ、hasOneならあり得る?』かというと、そういうものではありません。
hasOneの場合、どっちに外部キーを置けばよいか分かりづらいため、外部キーを頼りに考えていくと、より混乱をきたしやすいようです。
そういう場合は、心の中で『hasOne は、hasMany の特殊な一形態』と唱えるようにしましょう。
だって、相手方(関連モデル)が『複数くっついているか、ひとつしかくっつかないか』の差でしか無いわけですから、hasOneなら自モデルに外部キーとなるはずはありません。
もちろんhasOneとhasManyはもっと本質的に違いますが、ここではどのようにアソシエーションすればよいかを考えるために単純化しています
外部キーの位置関係のまとめ
外部キーは、hasOneおよびhasManyのときには『相手方のモデルに存在』し、belongsToしているときは『自モデルに存在』するのです。
この原則を守れば、先頭で提示したマスタテーブルの問題も簡単に答えを導けます。
なお、なぜ上記の原則が正しいのかは、ここでは深く追求しませんのであしからず
マスタテーブルはhasOneではなくhasMany
答えを先に言ってしまいました。
それでは順を追って、なぜそうなるのかを説明していきたいと思います。
例の確認
- ブログの記事を扱ったArticleモデルと、その記事のステータスを扱うStatusモデルがある
- ArticleモデルとStatusモデルのアソシエーションを検討する
- Statusモデルは、editing , published , deleted の3レコードのみからなるマスタテーブル
選択肢はみっつ
hasOneと仮定した場合
- Article hasOne Status
- Status belongsTo Article
hasManyと仮定した場合
- Article hasMany Status
- Status belongsTo Article
belongsToと仮定した場合
- Article belongsTo Status
- Status has??? Article
1: 上記の原則を当てはめて考える
まず、外部キーをどちらに設定するかを考えます。
ArticleかStatusか、どちらになるのかは、あまり考える必要が無いでしょう。
マスタテーブルに外部キーを設定する可能性はありませんから。
つまりStatusには外部キーが存在しないと言うことになります。
ということは。
Article belongsTo Status が確定します。
2: Status has??? Article は、hasOne ? hasMany ?
逆に言えば、Status has??? Article ということになります。
では、hasOneなのかhasManyなのかが問題となりますが、この答えも自明でしょう。
記事は無限に増え続けるわけですから、hasManyでないと、statusが足りません(笑)
Status hasMany Article が確定します。
3: ふたたび上記の原則を当てはめて検証する
結果的にhasManyを用いて、以下のようになることが分かりました。
- Article belongsTo Status
- Status hasMany Article
当初イメージした『ArticleがStatusを持っている』という、やってしまいがちな解釈からいくと、逆になりましたね。
では、外部キーがどうなっているかを考えましょう
-
Article
- id
- status_id
- title タイトル
- body 本文
- Other その他
-
Status
- id
- name
はい。間違いなく、上記の原則に沿っています。
それでも新人くんが迷っていたら
1: hasOneは関係を逆にしてもhasOne
hasOneかhasManyかですが、次のような考え方も成り立ちます。
CakePHPのアソシエーションには『1対多』は存在しても『多対1』はありませんよね。
一方でhasOneは『1対1』ですから、User hasOne Profile が存在する場合。それは、Profile hasOne User でもあるわけです。
つまり『関係性をひっくり返してみて成り立つかどうか』で、A hasOne B か A belongsTo B にするべきかを判断することが可能といえます。
上述のArticleとStatusの例で、Article hasOne Status と(間違って)考えたとしても、逆の関係性=『Status hasOne Article が成り立たないから間違っている』と判断することが可能です。
そんなときはArticle hasMany Status なんて突拍子もないことを考えることなく、Article belongsTo Status ( = Status has??? Article)と考えることができますね。
2: hasMany の場合は、Counter Cache を考える
Article hasMany Comment というアソシエーションがあるとき、Article モデルに comment_count というフィールドを設置し、Commentモデルの(belongsTo)アソシエーション時にcounterCache => trueとすると、commentsテーブルにコメントが追加されたり削除されたりするたびに、自動的にArticle.comment_count にコメント数が入ります。
Articleモデル単体を呼び出すだけで、コメント数が分かるようになり、非常に便利です。
Commentモデルを一緒に引っ張ってきた場合でも、すべてのコメントを取得することなくコメント数が分かります
上述の例に戻ると、たとえば『各ステータス別の記事数が知りたい』という場合、以下のようにするでしょう。
# Status Model
# * id
# * name
# * article_count
$statuses = $this->Status->find('list', array('fields' => array('Status.name', 'Status.article_count'));
debug( $statuses );
// example
// editing => 3
// published => 35
// deleted => 7
Status hasMany Article が正解ということは、Counter Cacheが可能であることからも分かりますね。
まとめ
アソシエーションの基本原則
- 外部キーを持っているときはbelongsTo
- hasOneおよびhasManyの相手方のモデルには外部キーが存在する
迷ったら
- マスタテーブルはhasMany
- 外部キーから考える
- hasOneで良いのか迷ったら、関係性を逆にしてhasOneが成り立たなかったら、belongsTo
- hasManyに違和感があったら、Counter Cache が設定できないかを考え、設定できればhasManyで問題ない
以上です。
アソシエーションは油断すると中級者でも混乱します。
CakePHPに不慣れな新人さんが、アソシエーションで迷っていたときの参考に是非。
参考
関連: モデルを結びつける :: モデル :: CakePHPによる開発 :: マニュアル :: 1.2 Collection :: The Cookbook
【CakePHP】アソシエーションで迷ったらこう考えよう | ECWorks Blog
CakePHPでRSS2.0 feed
CakePHPでRSSフィードをはくのは、ものすごくカンタンです。
今回、当ブログで身をもって体験しましたので、ぜひ。
とはいえ、すでに他のサイトでRSSについては、記事になっています。
(わたしも今回参照しながら対応させていただきました。m(_ _)m)
ぜひ、以下のサイトを参考に設定してみてください。
- RssHelper で RSS フィードを生成する :: RSS :: 主要なヘルパー :: マニュアル :: 1.2 Collection :: The Cookbook
- CakePHP1.2でRSS2.0を出力する[RSS][CakePHP] | Web&MUSICブログ QUALL
- CakePHPのRSSヘルパーの使い方まとめ - 頭ん中
基本的な流れは、上記のサイトの方が詳しいので、そちらを見ていただくとしまして、今回は、Web&MUSICブログ QUALLさんのところで対応していたCDATAについての別解です。
まずは、RSS配信の流れから
ほぼ、上述のサイトそのままですが。
config/routes.phpを編集する
# config/routes.php // 追記 Router::parseExtensions();
controllerでRequestHandlerコンポーネントを読み込む
articles_controller に記事があるとします。
# controllers/articles_controller.php or app_controller.php
// 例
var $components = array('RequestHandler',);
viewにRSS用のviewを作成
レイアウトファイルに標準のものを使用する場合は、viewファイルだけを作ります。
ここで、view内にfunctionを書くのに違和感ある場合は、helperに記入すると良いでしょう。
以下は、そんなケースの例です。
ここではMyHelper (my.php) というのがあるとします。
# views/articles/rss/index.ctp
// 記事一覧が $this->data に入っている場合
echo $rss->items($this->data, array($my, 'rss_transform'));
# views/helpers/my.php
<?php
class MyHelper extends AppHelper {
function rss_transform($item)
{
return array(
'title' => $item['articles']['title'],
'link' => array('action' => 'view', $item['articles']['id']),
'description' => array(
'value' => $item['articles']['body'],
'cdata' => true,
'convertEntities' => false,
),
'pubDate' => ['articles']['posted_date'],
);
}
以上でフィードがはかれます。
デフォルトURLは、 上記の例なら、 /articles/index.rss です。
CDATA対応
上記の description のように配列で記述すると、HTMLタグがそのまま入ります。
HTMLタグをエスケープしたくない場合は、convertEntities を false に、中身をCDATAで囲う場合はcdata を true に、それぞれ設定してください。
というわけで、以上です。
このブログもRSS配信をするために、数時間で対応できました。(現在フィードのテスト中)
上述したブログのおかけで、実際の配信部分は30分程度で完了し、残りはRoutingに苦労したのと、CDATAに対応するために試行錯誤しただけ。
CakePHPさまさまです。
CakePHPのバージョンを取得する方法
CakePHPでプラグインを作っていると、現在稼働中のCakePHPのバージョンが1.2なのか1.3なのか知る必要がでてくるかもしれません。
そんなとき、どうやってCakePHPのバージョンを取得するのだろうと思って、IRCで聞いたら、教えてもらいました。
CakePHPのバージョン取得
debug( Configure::version() ); # ex : 1.2.3.8166
Configureクラスを見てみると、どうやら、coreのconfig/config.phpに値がある様子。
で、ソースを見てみると、、、
<?php return $config['Cake.version'] = '1.2.3.8166'; ?>
コメント除くとこれだけでした。
CakeMatsuriは有意義で楽しかった
ブログを書くまでが勉強会であり、ブログを書くまでがCakeMatsuri(祭り)です。
ようやくブログを書き上げた。いまだ、祭り気分が抜けていなかったから、というわけではありませんが。
お祭りもようやく終了です。
私が参加したのは2日目のカンファレンス。
事例紹介等の通常のセッションの他にも、CakePHPのコアデベロッパーの話を直接聞けるということもあり、盛りだくさんの内容。
セッションの様子は、他の方のブログに任せまして、CakeMatsuriで一番得られたものについて書きたいと思います。
私が参加した理由のひとつは、CakePHPの勉強以上に、CakePHPを使っている方々との交流にありました。
せっかくユーザー数が増えているCakePHPです。使っている方とひとりでも多くお話しするに越したことはありません。さらに、コアデベロッパーともお話しできるチャンス。これを逃す手はありません。(まあ英語できないんですけどね。そこは気持ちで押し切ろうと)
さて、カンファレンス当日。
早めに家を出たつもりも、着いてみたら開始5分前。席はまばらでしたが、ちょうど良い時間でした。
cakephperさんに受付してもらい、CakeMatsuri特製手ぬぐいをもらいます。
2種類あって悩みましたが、より祭りっぽい方をチョイス。これはうちに帰って家族にプレゼント。(デザインが素敵だと喜ばれた)
席に着くとほどなくして、隣に外国の方が!日本の人と話ながら座りました。
さすがCakeMatsuriです。インターナショナルですな。
でも、ばりばり日本語が堪能でした。
昼食時に3人で乾杯しお弁当をいただきながらいろいろお話しできました。日本語で。
聞くと、イギリス生まれで7~8年前から日本で働いているとのこと。
着くなりMacBookを開け、おもむろにWindowsを立ち上げました(笑)
何と、USBに開発ツールを始めとしたアプリをすべて詰め込み、家(win)でも会社(Mac)でも同じ環境で作業できるんだそうです。ソノハッソウハナカッタワ。
わたしも今ではその人に教わったportableappsで、USB開発環境を作ってしまいました。Macは持ってないけど取引先のPCでもネットカフェでも開発ができるっていうのは、便利すなあ。
何せUSBさえ持っていけばgitも使えるんですから、ポータブル環境最高です。
さらに、会社でもMacBookにキーボードとディスプレイを外付けしてUSBに入れたアプリで作業していると、笑いながら話してくれました。うん、Macの意味ないですね(笑)
でも見ててMac欲しくなった。
ちなみに会場に来ている方のノートPCはほとんどMacBook。私は今のところノートPCを持たないでやっていけてるのですが、必要になったらこりゃ初Macですね。開発しやすそうだし。
ちなみにちなみに私が持って行ったのはPomera。テキストエディタだけです(恥)
もちろんひとりぼっち。
この陽気なイギリスの方とは、この日いちにち最後まで一緒でした。
そしてこの方と談笑しながら席に着いたのが、帰ってきてからこれ書いた人だと分かって大層びっくりしたヨシダスタジオの中の人。
大体同じくらいのCakePHP歴で、しかも、Croogoの話を聞いた後は、CakePHP製のCMSというお互いの興味がかぶっていることも判明。
職場も私の出身高校の近くということで何かと縁のある方。
さっきサイト見たらTwitter始めたらしいからさっそくフォローした。
カンファレンスの最後のプログラムは懇親会。
ここで、他にも多数の方とお話しできた。
なかには私と同姓同名(漢字一字違い)という方も。よくある名字とよくある名前の組み合わせでも、なかなか同姓同名には当たらないものです。
もしかしたら人生で初かも。
懇親会ではおもに、コアデベロッパーにアタック。
ここで、最近英語に触れてなかったことに激しく後悔。
簡単な表現すらでてこない。orz
気持ちでは押し切れないということが分かりました。
で、何とか話をしたのは。
まず、Grahamさん(@Predominant)とは。
CakePHP1.3やCakePHP2.0ではNameSpaceがサポートされるのかという質問をしたところ、両方ともPHP5.2をサポートするから使わないというお返事。
そうだよね。そうすると結局名付け問題には当分気を遣うことになりそうです。
あとは逆にCakePHPについての不満は?と聞かれてしまう。
実は、正直言って、今のところ不満はないんです。
で、「ない」と応えると、「It's Perfect???(さすがに完璧ってことはないよね?)」と言われてしまった。
「いやあ、パフォーマンスもキャッシュ使ったりで何とかやっているので問題ないんだ、今のところは」って言おうとして、3歳児レベルの片言の英語で必死に伝えようとするものの、伝わるわけはありませんので、最終的には「Yes. Perfect!」といって笑ってごまかした。
でも、Grahamさんは、そんな私のつたない英語を必死に聞いてくれて、ああ、心底良い人だと思いましたよ。わたしは。
イケメンJoelさん(@jperras)とは。
「今までKey Valueストアを避けてきたけど、これからは使います」と、言いたくて、後半だけ伝えた。
「ぜひ使ってみてよ」と笑顔で言われた。
Joelさんは話をするとき、まっすぐ目を見て話す。真剣さが伝わってくる。見習いたい。
他にもいっぱいしゃべったが覚えていないのは、昼からビールを飲み続けたからかも知れない。
そして、2次会にもお邪魔した。
ここでは日本の食事や食材の話が多かった。
いわし明太子やらゴーヤチャンプルやら烏賊の一夜干しやら、続々と「食べたこと無いよね」という料理が注文される中、Grahamさんのベストは「たこわさ」だった。
日本食には全般的に満足してくれたみたいだ。しかもヘルシーすぎて日本から帰ったら7kgもやせていたらしい。
なんで日本人(とくに私)は、日本食で太れるのか。
振り返ってみると、2次会で見たコアデベロッパーの素の姿がとても印象的。
いたって普通だ。いや普通じゃない。いたって良い人たちだ。
デベロッパーが、人間的にも素晴らしいから、CakePHPのプロジェクトは成功しているんだと、改めて思った。
いつか海外のお祭りにも参加してみたい。
この日の解散は深夜0時に近かった。
2週間以上経っても、一瞬たりとも色あせない。とても有意義な一日だった。
運営に携わったすべての方々へ
本当に楽しめたお祭りでした。直接御礼の言葉を言えませんでしたが、どうもありがとうございました。
gitでhookを使ってWebサイトの自動更新
以前のエントリでは、最終的に、テスト環境および本番環境で git pull することによって、それぞれを更新しています。
しかし、git には、push等が実行されると、その後に自動実行してくれるフックメカニズムが用意されています。
これを利用すれば、いちいちbareリポジトリにpush後、手動でgit pullすることなく、自動的に行なわせることができます。
下準備(テスト用と本番用でbranchを分ける)
これは必ずしも同じようにやる必要はありませんが、私の場合は、テスト環境用と本番環境用で、それぞれbranchを分けることにしています。
本番環境用を masterブランチ、テスト環境用を developブランチとします。
まずは、ブランチを作りましょう。
#localhost # 現在のブランチを確認する $ git branch -l * master $ git checkout -b develop $ git branch -l master * develop $ git push origin develop #server $ cd /var/dev ;#テスト環境 $ git checkout -b develop origin/develop
開発はdevelopブランチもしくは、developブランチから分岐したブランチで行ない、テスト環境でのチェック後、developブランチをmasterブランチへmergeします。
#localhost $ git branch -l master * develop $ edit;edit;edit; $ git commit -a; $ git push origin develop #server $ cd /var/dev ;#テスト環境 $ git pull origin develop ;#後述のフックを使用し不要になる操作 #本番サーバーでのテストが完了したら #localhost $ git cehckout master $ git merge develop $ git push origin master #server $ cd /var/www ;#本番環境 $ git pull origin master ;#後述のフックを使用し不要になる操作
これでふたつのブランチで運用することができました。
hookを使用する
hookは、bareリポジトリに設定します。
#server $ cd /repos/bs.git ;#bareリポジトリ $ cp -p hooks/post-update.sample post-update $ vi hooks/post-update # 追記 (cd /var/dev && git --git-dir=.git pull origin develop) (cd /var/www && git --git-dir=.git pull origin master)
これで終了です。
実際にpullされるかを試してみましょう。
#localhost $ git checkout develop $ edit;edit;edit; $ git commit -a $ git push origin develop # このときにテスト環境がdevelopブランチをpullしていることが確認できます。 $ git checkout master $ git merge develop $ git push origin master # このときに本番環境がmasterブランチをpullしていることが確認できます。 $ git checkout develop ;#開発は常にmaster以外のブランチで行なう
めでたく自動化できました。
一度設定すると、本番サーバーでの作業が激減しますね。
さくらインターネットで git push するとgit-receive-pack: Command not found. と言われる問題の応急処置
さくらインターネット(Sakura Internet)で、git push すると、git-receive-pack: Command not found.と表示されてうまくいかないという問題があります。
問題の原因としては、gitコマンドへのパスが通っていないためなのですが、その解決方法が分からずじまい。
この問題については、以下のようなサイトが参考になります。
git [てきとうにめも]
問題の原因が分かっても、どうにも対処できなかったので、pushする側で対処しました。
#localhost
$ cd
$ vi .gitconfig
# 以下を追加
[alias]
spush = push --receive-pack=/home/username/local/git/bin/git-receive-pack
sclone = clone --upload-pack=/home/username/local/git/bin/git-upload-pack
これで git spush origin master とすればreceive-packを指定したことになります。
#localhost $ git commit;git commit;git commit $ git spush origin master $ git spush --all $ git spush --tags
根本的解決が分かったらまたブログ書く。