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